<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>thomas-leister.de</title><link>https://thomas-leister.de/</link><description>Das persönliche Weblog zu den Themen Linux, Server und freier / offener Software</description><generator>Hugo -- gohugo.io</generator><language>de</language><managingEditor>thomas.leister@mailbox.org (Thomas Leister)</managingEditor><webMaster>thomas.leister@mailbox.org (Thomas Leister)</webMaster><lastBuildDate>Sat, 07 Feb 2026 22:43:33 +0100</lastBuildDate><atom:link href="https://thomas-leister.de/" rel="self" type="application/rss+xml"/><item><title>Mailserver mit Dovecot, Postfix, MySQL und Rspamd unter Debian Trixie</title><link>https://thomas-leister.de/mailserver-debian-trixie/</link><pubDate>Sat, 07 Feb 2026 22:43:33 +0100</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/mailserver-debian-trixie/</guid><description>&lt;p&gt;Es geht weiter in der Serie meiner Mailserver-Tutorials. Nachdem meine letzte Anleitung zu Debian Bullseye schon einige Jahre alt ist, ist es an der Zeit, sich dem Thema nochmal zu widmen - in Hinblick auf das nun aktuelle Debian Trixie.&lt;/p&gt;
&lt;p&gt;Für alle, die schon den Mailserveranleitungen aus den letzten Jahren gefolgt sind: Euer Mailserver lässt sich mit wenig Aufwand gut auf den Stand aus dieser Anleitung aktualisieren. Das Datenbank-Layout hat sich (mit Ausnahme der entfernten tlspolicies Tabelle) nicht geändert und auch sonst sind alle Datenstrukturen aus den Verzeichnissen gleich geblieben - einzig der Funktionsumfang hat sich leicht geändert und die Konfigurationen wurden auf den aktuellen Stand der Technik angepasst. Außerdem wurde die Anleitung umformuliert und an einigen Stellen aus Gründen der Übersichtlichkeit gekürzt. Alle Änderungen seit der letzten Version meiner Anleitung könnt ihr im &lt;a href="https://thomas-leister.de/mailserver-debian-trixie/#changelog-im-vergleich-zur-letzten-anleitung-mailserver-unter-debian-bullseye-v12"&gt;Changelog&lt;/a&gt; am Ende des Beitrags einsehen.&lt;/p&gt;
&lt;h2 id="was-und-für-wen"&gt;Was und für wen?&lt;/h2&gt;
&lt;p&gt;Mithilfe dieser Anleitung werdet ihr in der Lage sein, einen einfachen Open Source basierten Mailserver unter Debian Trixie aufzusetzen. Der Server wird euch ermöglichen, eure E-Mails unter einer eigenen Domain selbst zu hosten. Was der Featureset angeht, bekommt ihr eine solide Grundausstattung, die sich bei Bedarf problemlos um weitere Features erweitern lässt.&lt;/p&gt;
&lt;p&gt;Was ihr bekommt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Senden und Empfang von E-Mails über einen E-Mail Client (z.B. Thunderbird, Evolution, Outlook, &amp;hellip; K9 Mail, etc)&lt;/li&gt;
&lt;li&gt;Moderne Spamabwehr inkl. Spam Learning&lt;/li&gt;
&lt;li&gt;DKIM-Signierung ausgehender E-Mails für bessere Akzeptanz bei fremden Mailsystemen und Fälschungssicherheit&lt;/li&gt;
&lt;li&gt;Filterregeln / Autoresponder bei Urlaub / &amp;hellip;&lt;/li&gt;
&lt;li&gt;E-Mail Weiterleitungen (Aliase)&lt;/li&gt;
&lt;li&gt;Verwaltung aller Mailaccounts und Weiterleitungen / Aliase über eine MySQL-Datenbank.&lt;/li&gt;
&lt;li&gt;Sichere Verschlüsselung der Kommunikation zwischen E-Mail Servern, falls verfügbar&lt;/li&gt;
&lt;li&gt;Größenbeschränkung von Postfächern&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Send-only&amp;rdquo; Account, die nur zum Senden, aber nicht zum Empfangen vorgesehen sind (z.B. für Nextcloud, Forensoftware, &amp;hellip;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Was ihr mit dieser Anleitung &lt;strong&gt;nicht&lt;/strong&gt; bekommt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eine vollständige E-Mail Suite mit Webmail, Office, Kalender, Kontakte &amp;hellip;&lt;/li&gt;
&lt;li&gt;Einen Server zum massenhaften Senden von E-Mails (Newsletter etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Zielgruppe&lt;/strong&gt; dieser Anleitung sind professionelle Linux-Systemadministratoren und Hobbyisten, die verstehen wollen, wie ihr Mailsetup funktioniert und einen sicheren E-Mail Server ohne Schnickschnack aufsetzen wollen.&lt;/p&gt;
&lt;p&gt;Für all diejenigen, die eigentlich lieber &amp;ldquo;nur kurz etwas installieren&amp;rdquo; würden und sich eher ein Rundumpaket wünschen, das die Komplexität eines E-Mail Servers versteckt, ist möglicherweise ein &lt;a href="https://mailcow.email"&gt;Docker-basierter Mailcow-Server&lt;/a&gt; besser geeignet.&lt;/p&gt;
&lt;h2 id="voraussetzungen-für-den-betrieb-eines-e-mail-servers"&gt;Voraussetzungen für den Betrieb eines E-Mail Servers&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Je nach Anzahl der Nutzer ein passender vServer oder physischer Server. Für eine zweistellige Nutzeranzahl reicht ein Quadcore-Server mit 4 GB RAM bereits völlig aus. Für eine kleine bis mittlere zweistellige Nutzerzahl reicht auch ein Raspberry Pi schon aus. Passende Hoster sind z.B. Hetzner, OVH, Scaleway.&lt;/li&gt;
&lt;li&gt;Eine vernünftige Internetanbindung mit &lt;strong&gt;statischer IP-Adresse&lt;/strong&gt; und der Möglichkeit, einen &lt;strong&gt;Reverse-DNS (PTR) Record&lt;/strong&gt; für diese Adresse zu setzen (idR. trifft das auf Privatkundenanschlüsse bei denen gängigen ISPs &lt;em&gt;nicht&lt;/em&gt; zu!)&lt;/li&gt;
&lt;li&gt;Eine Domain, für die beliebige DNS-Records gesetzt werden können&lt;/li&gt;
&lt;li&gt;Vorinstalliertes Debian 13 &amp;ldquo;Trixie&amp;rdquo;. Die &amp;ldquo;minimal&amp;rdquo; bzw. Server-Version reicht aus - eine grafische Oberfläche wird nicht genutzt. Am besten eine frische Installation ohne Vorbelastung.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="annahmen-und-bezeichnungen"&gt;Annahmen und Bezeichnungen&lt;/h2&gt;
&lt;p&gt;Um der Anleitung besser folgen zu können, treffe ich einige Annahmen zum Setup.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Primäre Domain, die für den Mailserver genutzt wird: &lt;code&gt;mysystems.tld&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(Optionale) weitere Domains, für die ebenfalls Postfächer betrieben werden sollen: &lt;code&gt;domain2.tld&lt;/code&gt;, &lt;code&gt;domain3.tld&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Hostname des Mailservers: &lt;code&gt;mail.mysystems.tld&lt;/code&gt; &lt;em&gt;(ich empfehle, &lt;code&gt;mail&lt;/code&gt; als Subdomain zu nutzen!)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Öffentliche IPv4-Adresse: &lt;code&gt;203.0.113.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Öffentliche IPv6-Adresse: &lt;code&gt;2001:db8::1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;All diese Domains und IP-Adressen sind selbstverständlich auf die eigenen Gegebenheiten anzupassen. Achtet also im Laufe der Anleitung darauf, diese &lt;em&gt;immer&lt;/em&gt; durch eure eigenen Domains oder Adresse zu ersetzen!&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="mailserverkomponenten-und-ihre-aufgaben"&gt;Mailserverkomponenten und ihre Aufgaben&lt;/h2&gt;
&lt;h3 id="dovecot"&gt;Dovecot&lt;/h3&gt;
&lt;p&gt;Dovecot ist ein weit verbreiteter MDA (Mail Delivery Agent) und IMAP-Server. Er sortiert ankommende E-Mails in die Postfächer des jeweiligen Empfängers ein und stellt eine Schnittstelle zum Abrufen der Mailbox bereit (IMAP). Außerdem wird Dovecot in diesem Setup von Postfix als sog. SASL-Authentifizierungsserver genutzt: Postfix fragt Dovecot, ob ein bestimmter Benutzer berechtigt ist, sich am System anzumelden.&lt;/p&gt;
&lt;h3 id="postfix"&gt;Postfix&lt;/h3&gt;
&lt;p&gt;Postfix wird oft zusammen mit Dovecot eingesetzt. Der populäre MTA (Mail Transfer Agent) kümmert sich um alles, was mit dem Transport der E-Mail zu tun hat: Vom E-Mail Client zum eigenen Mailserver, und von dort aus zum jeweiligen Zielserver. Außerdem nimmt Postfix E-Mails von fremden Servern an und leitet sie an den MDA Dovecot weiter. Antispam-Software wird i.d.R. direkt in Postfix integriert, um eintreffende Spammails erst gar nicht in die Mailbox des Nutzers gelangen zu lassen.&lt;/p&gt;
&lt;h3 id="mariadb-mysql-datenbank"&gt;MariaDB (MySQL-Datenbank)&lt;/h3&gt;
&lt;p&gt;Dovecot und Postfix werden so konfiguriert, dass sie eine MySQL-Datenbank als Backend (Datenbasis) nutzen. In der Datenbank werden zu nutzende Domains, Benutzer, Aliase und weitere Daten gespeichert. Durch einfaches Hinzufügen oder Entfernen von Datensätzen in oder aus Datenbanktabellen können neue Benutzer oder Aliase angelegt oder gelöscht werden. Der Vorteil eines Datenbank-Backends ist, dass sich der Mailserver damit sehr einfach verwalten lässt: So ließe sich zur Benutzerverwaltung beispielsweise eine Weboberfläche in PHP entwickeln, die die MySQL-Datenbank verändert. Die Serverkonfiguration muss dann nicht manuell geändert werden.&lt;/p&gt;
&lt;h3 id="rspamd"&gt;Rspamd&lt;/h3&gt;
&lt;p&gt;Rspamd ist ein Filtersystem, das in Postfix integriert wird und eingehende E-Mails überprüft. Spammails werden von Rspamd erkannt und nicht an den Benutzer zugestellt bzw. aussortiert. Außerdem fügt Rspamd bei ausgehenden E-Mails eine DKIM-Signatur hinzu, sodass fremde Mailserver eigene Mails nicht als verdächtig einstufen.&lt;/p&gt;
&lt;h3 id="nginx"&gt;Nginx&lt;/h3&gt;
&lt;p&gt;Nginx ist ein weit verbreiteter und schlanker Webserver / Webproxy. In diesem Setup hat er mehrere Aufgaben: Zum einen kann er als Endpunkt für den Ausstellungsprozess von Let’s Encrypt-Zertifikaten dienen, zum anderen als Proxy für die Rspamd-Weboberfläche.&lt;/p&gt;
&lt;h3 id="redis"&gt;Redis&lt;/h3&gt;
&lt;p&gt;Redis ist ein hochperformanter In-memory Key-Value-Store, also eine sehr einfache, schnelle Datenbank, welche Schlüssel-Wert-Paare effizient im Speicher ablegen kann. Rspamd nutzt Redis, um einige Daten zwischenzuspeichern (wie z.B. Statistiken, &amp;ldquo;gelernte&amp;rdquo; Spammails, &amp;hellip;).&lt;/p&gt;
&lt;h2 id="vorbereitungen"&gt;Vorbereitungen&lt;/h2&gt;
&lt;p&gt;Bevor es mit dem Setup der einzelnen Mailserverkomponenten losgeht, muss das System noch ein Stück weit vorkonfiguriert werden. Wie oben bereits erwähnt, empfiehlt sich die Installation auf einem frischen Debian Trixie System, d.h. ohne Vorbelastungen, ohne einen parallel Betriebenen Webserver oder ähnliches. So kann am besten sichergestellt werden, dass die Schritte aus dieser Anleitung zum Ziel führen.&lt;/p&gt;
&lt;h3 id="login-als-root"&gt;Login als root&lt;/h3&gt;
&lt;p&gt;Loggt euch über SSH auf eurem Debian System ein. Falls ihr euch nicht eh schon direkt als User &lt;code&gt;root&lt;/code&gt; einloggt, sorgt dafür, dass ihr eine Root-Shell bekommt:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo su
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="aktualisierung-des-systems"&gt;Aktualisierung des Systems&lt;/h3&gt;
&lt;p&gt;Das System soll nicht direkt nach Fertigstellung schon veraltet und Sicherheitslücken ausgeliefert sein, richtig? Aktualisiert euer Debian auf den aktuellen Stand:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt update &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="hostnamen-und-server-fqdn-setzen"&gt;Hostnamen und Server-FQDN setzen&lt;/h3&gt;
&lt;p&gt;Damit der Mailserver Mails der eigenen Nutzer vernünftig erkennen kann, muss das Betriebssystem wissen, wie es heißen soll. Dazu wird der Hostname &lt;code&gt;mail.mysystems.tld&lt;/code&gt; gesetzt:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hostnamectl hostname --static mail
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In der Hosts-Datei &lt;code&gt;/etc/hosts&lt;/code&gt; sollten FQDN und lokaler Hostname hinterlegt sein. Die Datei kann beispielsweise so aussehen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1 localhost
127.0.1.1 mail.mysystems.tld mail
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Die Ausgaben der Kommandos &lt;code&gt;hostname&lt;/code&gt; und &lt;code&gt;hostname --fqdn&lt;/code&gt; sollten nach den Änderungen wie folgt aussehen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@mail:~# hostname
mail
root@mail:~# hostname --fqdn
mail.mysystems.tld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Der FQDN (in diesem Beispiel &amp;ldquo;mail.mysystems.tld&amp;rdquo;) wird außerdem nach &lt;code&gt;/etc/mailname&lt;/code&gt; geschrieben:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;hostname -f&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt; &amp;gt; /etc/mailname
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;(Der Hostname im Shell-Prompt z.B. &lt;code&gt;root@meinhost:&lt;/code&gt; passt sich erst nach einem erneuten Login an.)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="unbound-dns-resolver-installieren"&gt;Unbound DNS Resolver installieren&lt;/h3&gt;
&lt;p&gt;Das Mailsystem benötigt einen funktionierenden DNS-Server (Resolver), sodass die Herkunft von E-Mails überprüft werden kann. Auch der Rspamd-Spamfilter verlässt sich bei der Bewertung von Spammails auf DNS-Dienste. Eine schnelle Namensauflösung bringt also Performancevorteile für das ganze Mailsystem. Für den Zugriff auf Spamhaus-Blocklists kann es sogar notwendig sein, seinen eigenen DNS-Resolver zu nutzen, weil z.B. Zugriffe über das Google DNS blockiert werden. Deshalb (und um die Sicherheit im Bezug auf Abhängigkeiten zu fremden Systemen zu verbessern) empfehle ich die Nutzung eines eigenen, lokalen DNS-Resolvers.&lt;/p&gt;
&lt;p&gt;Unbound installieren:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt install unbound
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="tip"&gt;
Für die nächsten Kommandos muss müssen die Pakete &lt;code&gt;dnsutils&lt;/code&gt; und &lt;code&gt;resolvconf&lt;/code&gt; installiert sein. Wenn bei der Installation von Debian die &amp;ldquo;Standard-Systemwerkzeuge&amp;rdquo; ausgewählt wurden, sind die &lt;code&gt;dnsutils&lt;/code&gt; schon installiert.
&lt;/div&gt;
&lt;p&gt;Ein &lt;code&gt;dig @::1 denic.de +short +dnssec&lt;/code&gt; sollte eine ähnliche Ausgabe wie folgt erzeugen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dig @::1 denic.de +short +dnssec
A 8 2 3600 20170814090000 20170731090000 26155 denic.de. Jo90qnkLkZ6gI4qNHj19BMguFuGof9hCPhdeSh/fSePSQ/WXlWMmfjW1 sNDJ/bcITRMyz8DQdDzmWPDIeSJ/qPyfoZ+BjUZxtaXcs0BAl4KX8q7h R05TGmAbgPhrYBoUKJkU/q8T+jWKHAJRUeWbCd8QOJsJbneGcUKxRAPe i6Rq51/OL/id6zUCtalhclah2TfLLaqku9PmKwjbGdZm11BXSr8b56LB WX/rdLIrKWNpE+jHGAUMmDsZL84Kx3Oo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wenn der &lt;code&gt;dig&lt;/code&gt;-Befehl funktioniert hat, kann der lokale Resolver als primärer Resolver gesetzt werden:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt install resolvconf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#34;nameserver ::1&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/resolvconf/resolv.conf.d/head
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ein &lt;code&gt;nslookup denic.de | grep Server&lt;/code&gt; sollte nun&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Server: ::1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;zurückliefern. Damit ist der lokale DNS-Resolver als Haupt-Resolver einsatzbereit.&lt;/p&gt;
&lt;h2 id="einrichtung-des-dns"&gt;Einrichtung des DNS&lt;/h2&gt;
&lt;p&gt;Zu Beginn dieser Anleitung wurde für den Mailserver der FQDN &amp;ldquo;mail.mysystems.tld&amp;rdquo; festgelegt. Für diesen Domain-Namen werden nun A-Records im DNS-Zonefile der Domain &amp;ldquo;mysystems.tld&amp;rdquo; erstellt. Loggt euch bei eurem Domain-Provider ein und legt die folgenden Einträge an – der erste für die IPv4-IP-Adresse des Mailservers, die zweite für die IPv6-Adresse. (Beispiel!):&lt;/p&gt;
&lt;p&gt;Achtet im Folgenden vor allem auf den abschließenden Punkt in den Domainnamen!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mail.mysystems.tld. 86400 IN A 203.0.113.1
mail.mysystems.tld. 86400 IN AAAA 2001:db8::1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;ldquo;mail.mysystems.tld&amp;rdquo; ist damit im DNS bekannt. Wenn keine IPv6-Adresse genutzt wird, kann der zweite Record entfallen. Bleiben noch &amp;ldquo;imap.mysystems.tld&amp;rdquo; und &amp;ldquo;smtp.mysystems.tld&amp;rdquo;, die als Alias-Domains für &amp;ldquo;mail.mysystems.tld&amp;rdquo; angelegt werden. Sie sind nicht unbedingt notwendig, werden von vielen Mailclients bei der Einrichtung eines neuen Accounts aber gesucht und erleichtern ggf. die Autokonfiguration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;imap.mysystems.tld. 86400 IN CNAME mail.mysystems.tld.
smtp.mysystems.tld. 86400 IN CNAME mail.mysystems.tld.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Mailclients&lt;/em&gt; können sich damit schon über imap.mysystems.tld und smtp.mysystems.tld zum Mailserver verbinden. Andere &lt;em&gt;Mailserver&lt;/em&gt; suchen bei der E-Mail-Übermittlung allerdings nicht nach A- oder CNAME-Records, sondern nach MX-Records. Ein MX-Record zeigt, welcher Mailserver für die E-Mails zu einer Domain zuständig ist. In meinem Beispiel soll sich unser Mailserver neben den E-Mails für mysystems.tld auch um die Mails für domain2.tld und domain3.tld kümmern.&lt;/p&gt;
&lt;p&gt;Im Zonefile der Domain &amp;ldquo;mysystems.tld&amp;rdquo; wird dazu dieser Record angelegt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysystems.tld. 86400 IN MX 0 mail.mysystems.tld.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In die Zonefiles der anderen Domains werden entsprechend die Records&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain2.tld. 86400 IN MX 0 mail.mysystems.tld.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;und&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain3.tld. 86400 IN MX 0 mail.mysystems.tld.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;angelegt. Wichtig ist hier wieder der abschließende Punkt nach den Hostnamen!&lt;/p&gt;
&lt;h2 id="reverse-dns"&gt;Reverse DNS&lt;/h2&gt;
&lt;p&gt;Ein DNS-Eintrag löst einen Hostnamen zu einer IP-Adresse auf. Nun geht es um den umgekehrten Weg: Das Auflösen einer IP-Adresse zu einem Hostnamen. Das ist wichtig, weil andere Mailserver idR. ermitteln, ob unser Mailserver tatsächlich der ist, der er vorgibt zu sein. Dazu wird die IP-Adresse unseres Servers zu einem Hostnamen aufgelöst und dann mit dem Hostnamen verglichen, den unser Server bei der Kontaktaufnahme präsentiert wird.&lt;/p&gt;
&lt;p&gt;Der Reverse DNS-Eintrag ist also unverzichtbar und sollte für &lt;em&gt;alle IP-Adressen&lt;/em&gt;, mit denen unser System nach außen kommuniziert, gesetzt sein. Vergesst also nicht, auch für eure ggf. vorhandene IPv6-Adresse einen passenden Eintrag anzulegen.&lt;/p&gt;
&lt;p&gt;Üblicherweise könnt ihr das Reverse DNS über die Verwaltungsoberfläche eures Serveranbieters pflegen oder über den Support bei eurem Netzbetreiber. Ist hier nicht der korrekte Hostname &lt;code&gt;mail.mysystems.tld&lt;/code&gt; für alle öffentlich genutzten IP-Adressen eingetragen, schlägt der Versand zu den meisten anderen Mailsystemen fehl! Hier ist also besondere Vorsicht geboten.&lt;/p&gt;
&lt;h3 id="spf-records"&gt;SPF-Records&lt;/h3&gt;
&lt;p&gt;Im Kampf gegen Spam und Phishing wurde das sog. Sender Policy Framework entwickelt (Siehe auch Beitrag: &amp;ldquo;&lt;a href="https://thomas-leister.de/voraussetzungen-email-versand-gro%C3%9Fe-provider/"&gt;Voraussetzungen für den Versand zu großen E-Mail Providern&lt;/a&gt;&amp;rdquo;). Obwohl es sich nur als eingeschränkt brauchbar erwiesen hat, erwarten die meisten Mailprovider gültige SPF-Records für andere Mailserver und prüfen diese. SPF-Einträge werden im Zonefile aller Domains erstellt, für die ein Mailserver E-Mails verschickt, und geben an, welche Server für eine Domain sendeberechtigt sind. Für unsere Domain mysystems.tld wird der folgende Record im Zonefile von mysystems.tld angelegt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysystems.tld. 3600 IN TXT v=spf1 a:mail.mysystems.tld mx ?all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hiermit erhält nur der im A-Record &amp;ldquo;mail.msystems.tld&amp;rdquo; genannte Server für mysystems.tld eine Sendeberechtigung. Die Neutral-Einstellung &amp;ldquo;?all&amp;rdquo; sorgt dafür, dass E-Mails von anderen Servern trotzdem angenommen werden sollen. Damit gehen wir Problemen beim Mail-Forwarding aus dem Weg. Wir erstellen den SPF-Record also eigentlich nur, damit andere Mailserver unseren Server wegen des existierenden Records positiv bewerten – nicht, weil er seinen Nutzen entfalten soll.&lt;/p&gt;
&lt;p&gt;Falls vorhanden, wird in den Zonefiles der beiden Domains &amp;ldquo;domain2.tld&amp;rdquo; und &amp;ldquo;domain3.tld&amp;rdquo; jeweils dieser Record angelegt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;domain2.tld. 3600 IN TXT v=spf1 include:mysystems.tld ?all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Über das &amp;ldquo;include&amp;rdquo; wird der Original DNS Record der Domain mysystems.tld eingebunden.&lt;/p&gt;
&lt;h3 id="dmarc-records"&gt;DMARC Records&lt;/h3&gt;
&lt;p&gt;DMARC-Einträge bestimmen, was ein fremder Mailserver tun soll, wenn eine von ihm empfangene Mail nach SPF- und DKIM-Checks offenbar nicht vom korrekten Mailserver stammt (wenn der Absender also gefälscht wurde). Es ist vernünftig, andere Mailserver anzuweisen, solche E-Mails nicht zu akzeptieren:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;_dmarc.mysystems.tld. 3600 IN TXT v=DMARC1; p=reject;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Der Record wird entsprechend auch für die anderen Domains gesetzt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;_dmarc.domain2.tld. 3600 IN TXT v=DMARC1; p=reject;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Einen DMARC-Record mit einer abweichenden Policy könnt ihr euch unter &lt;a href="https://elasticemail.com/dmarc/"&gt;https://elasticemail.com/dmarc/&lt;/a&gt; selbst generieren lassen.&lt;/p&gt;
&lt;h2 id="erstkonfiguration-nginx-webserver"&gt;Erstkonfiguration Nginx Webserver&lt;/h2&gt;
&lt;p&gt;Wie bereits angemerkt, übernimmt Nginx in diesem Setup verschiedene Aufgaben. Für&amp;rsquo;s erste dient er aber nur als HTTP-Endpunkt, um Let&amp;rsquo;s Encrypt Zertifikate zu beantragen. Das ist zwar prinzipiell auch ohne Nginx im standalone-Modus möglich, macht uns aber das Leben einfacher, wenn wir ihn sowieso in Betrieb haben &lt;em&gt;(=&amp;gt; belegte Ports durch Nginx, ACME client &amp;hellip;)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Dazu wird Nginx installiert &amp;hellip;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; und unter &lt;code&gt;/etc/nginx/sites-available/mail.mysystems.tld&lt;/code&gt; eine neue Konfiguration mit diesem Inhalt angelegt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
# listen 443 ssl;
# listen [::]:443 ssl;
server_name mail.mysystems.tld imap.mysystems.tld smtp.mysystems.tld;
# http2 on;
# ssl_certificate /etc/acme.sh/mail.mysystems.tld/fullchain.pem;
# ssl_certificate_key /etc/acme.sh/mail.mysystems.tld/privkey.pem;
# add_header Strict-Transport-Security max-age=15768000;
# if ($ssl_protocol = &amp;#34;&amp;#34;) {
# return 301 https://$server_name$request_uri;
# }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Einige Zeilen sind zu diesem Zeitpunkt bewusst auskommentiert und werden später wieder aktiviert, sobald TLS-Zertifikate verfügbar sind.&lt;/p&gt;
&lt;p&gt;Der neue vHost kann nun aktiviert werden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s /etc/nginx/sites-available/mail.mysystems.tld /etc/nginx/sites-enabled/mail.mysystems.tld
systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="beantragung-der-tls-zertifikate-via-lets-encrypt"&gt;Beantragung der TLS-Zertifikate (via Let&amp;rsquo;s Encrypt)&lt;/h2&gt;
&lt;p&gt;Gültige TLS-Zertifikate von anerkannten Zertifizierungsstellen (CAs) sind heute für jeden Mailserver ein &amp;ldquo;muss&amp;rdquo;. Immer mehr Mailsysteme verweigern zurecht den Empfang über ungesicherte Verbindungen. Was früher (vor allem für den privaten Einsatz) noch ein bedeutender Kostenfaktor war, bekommt man heute Dank Let&amp;rsquo;s Encrypt kostenlos und sehr unkompliziert. Im folgenden beziehe ich mich daher auf die Generierung von Let&amp;rsquo;s Encrypt-Zertifikaten. Selbstverständlich können stattdessen auch Zertifikate anderer CAs genutzt werden.&lt;/p&gt;
&lt;p&gt;Für Postfix und Dovecot benötigen wir Zertifikate zu folgenden Domains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mail.mysystems.tld&lt;/li&gt;
&lt;li&gt;imap.mysystems.tld&lt;/li&gt;
&lt;li&gt;smtp.mysystems.tld&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Um die Zertifikate von der Let&amp;rsquo;s Encrypt Zertifizierungsstelle abholen, nutze ich das schlanke &lt;a href="https://github.com/acmesh-official/acme.sh"&gt;acme.sh Script&lt;/a&gt;. Dieses verfügt über einen &amp;ldquo;Nginx-Mode&amp;rdquo;, in dem das Script den Nginx Webserver automatisch so konfiguriert, dass die Domains von LE verifiziert werden können.&lt;/p&gt;
&lt;p&gt;acme.sh installieren:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://get.acme.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="tip"&gt;
Der Hinweis, welcher besagt, dass socat installiert werden solle, kann ignoriert werden. Socat wird in unserem Fall nicht benötigt.
&lt;/div&gt;
&lt;p&gt;Nach der Installation muss das Shell-Profil neu geladen werden, damit das &amp;ldquo;acme.sh&amp;rdquo; Kommando verfügbar gemacht wird:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source ~/.profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zertifikate beantragen und herunterladen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;acme.sh --issue --server letsencrypt --nginx \
-d mail.mysystems.tld \
-d imap.mysystems.tld \
-d smtp.mysystems.tld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zertifikate installieren:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /etc/acme.sh/mail.mysystems.tld
acme.sh --install-cert -d mail.mysystems.tld \
--key-file /etc/acme.sh/mail.mysystems.tld/privkey.pem \
--fullchain-file /etc/acme.sh/mail.mysystems.tld/fullchain.pem \
--reloadcmd &amp;quot;systemctl reload nginx; systemctl reload dovecot; systemctl reload postfix;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Infolge unseres Reload-Commands wird acme.sh versuchen, Dovecot und Postfix neu zu laden. Da beide Softwarepakete noch nicht installiert sind, werden euch Fehler angezeigt. Ihr könnt diese zum gegenwärtigen Zeitpunkt aber ignorieren.&lt;/p&gt;
&lt;p&gt;Die vorher im Nginx auskommentierten Zeilen in &lt;code&gt;/etc/nginx/sites-available/mail.mysystems.tld&lt;/code&gt; können durch Entfernen der Hashzeichen nun aktiviert und Nginx mittels&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx -t
systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;getestet und neu gestartet werden.&lt;/p&gt;
&lt;p&gt;Damit die automatische Zertifikatserneuerung funktioniert, wird noch der Cronjob für acme.sh aktiviert:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;acme.sh --install-cronjob
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="mysql-datenbank-einrichten"&gt;MySQL Datenbank einrichten&lt;/h2&gt;
&lt;p&gt;Informationen über zu verwaltende Domains, Benutzer, Weiterleitungen und sonstige Einstellungen soll der Mailserver aus einer MySQL-Datenbank ziehen. Das hat den Vorteil, dass der Server im laufenden Betrieb flexibel angepasst werden kann, ohne die Konfigurationsdateien ändern zu müssen. Die Datenbank ermöglicht uns außerdem ein virtualisiertes Mailserver-Setup: Die Benutzer auf den Mailservern müssen nicht mehr als reale Linux-Benutzer im System registriert sein, sondern nur noch in der Datenbank eingetragen werden.&lt;/p&gt;
&lt;p&gt;Als DBMS wird die neue Debian Standard-MySQL-Datenbank &amp;ldquo;MariaDB&amp;rdquo; installiert:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install mariadb-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nach der Installation sollte MariaDB bereits gestartet sein. Den Status könnt ihr mittels &lt;code&gt;systemctl status mariadb&lt;/code&gt; überprüfen. Falls das nicht der Fall ist, startet den Datenbankserver: &lt;code&gt;systemctl start mariadb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Standardmäßig kann sich nur der root-Systemuser am Datenbankserver anmelden. Authentifiziert wird er dabei automatisch via PAM, sodass keine zusätzliche Passworteingabe notwendig ist. Die einfache Eingabe von&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bringt euch in eine MySQL Root Shell. Über diese werden nun ein paar SQL-Befehle ausgeführt.&lt;/p&gt;
&lt;p&gt;Ein SQL-Kommando endet immer mit einem Semikolon &lt;code&gt;;&lt;/code&gt;. Mehrzeilige Befehle könnt ihr ohne weiteres einfach mit ENTER umbrechen, so wie sie im Folgenden dargestellt werden.
Achtet auf den Unterschied zwischen &amp;ldquo;Tick&amp;rdquo; (&lt;code&gt;'&lt;/code&gt;) und &amp;ldquo;Backtick&amp;rdquo; (&lt;code&gt;`&lt;/code&gt;) – der Backtick wird mit Shift + 2x Accent-Taste erzeugt. Kopiert die SQL-Statements am besten direkt in eure Zwischenanlage, statt sie mühsam abzutippen.&lt;/p&gt;
&lt;p&gt;Im ersten Schritt wird eine neue Datenbank &amp;ldquo;vmail&amp;rdquo; angelegt.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE vmail
CHARACTER SET 'utf8'
COLLATE utf8_unicode_ci;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ein neuer DB-User &lt;code&gt;vmail&lt;/code&gt; mit dem Passwort &lt;code&gt;vmaildbpass&lt;/code&gt; bekommt Zugriff auf diese neue Datenbank &lt;em&gt;(Wählt ein eigenes Passwort statt &amp;ldquo;vmaildbpass&amp;rdquo;!)&lt;/em&gt;:&lt;/p&gt;
&lt;div class="warning"&gt;
Nutzt für &lt;code&gt;vmaildbpass&lt;/code&gt; nur die Zeichen 0-9 a-z und A-z. Einigen Berichten nach können Sonderzeichen zu Problemen führen.
&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;GRANT SELECT ON vmail.* TO 'vmail'@'localhost' IDENTIFIED BY 'vmaildbpass';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alle weiteren Kommandos zu Erstellung der Datenbank-Tabellen sollen sich auf die eben erzeugte Datenbank beziehen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use vmail;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Das Mail-Setup soll 3 verschiedene Tabellen nutzen. Kopiert die SQL-Statements einzeln und nacheinander in die MySQL-Kommandozeile und bestätigt jedes mal mit [Enter]. Anpassungen sind nicht notwendig.&lt;/p&gt;
&lt;h3 id="domain-tabelle"&gt;Domain-Tabelle&lt;/h3&gt;
&lt;p&gt;Die Domain-Tabelle enthält alle Domains, die mit dem Mailserver genutzt werden sollen.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `domains` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`domain` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`domain`)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="account-tabelle"&gt;Account-Tabelle&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `accounts` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(64) NOT NULL,
`domain` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`quota` int unsigned DEFAULT '0',
`enabled` boolean DEFAULT '0',
`sendonly` boolean DEFAULT '0',
PRIMARY KEY (id),
UNIQUE KEY (`username`, `domain`),
FOREIGN KEY (`domain`) REFERENCES `domains` (`domain`)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Die Account-Tabelle enthält alle Mailserver-Accounts. Das Feld &amp;ldquo;quota&amp;rdquo; enthält die Volumenbegrenzung für die Mailbox in MB (Megabyte). Im Feld &amp;ldquo;enabled&amp;rdquo; wird über einen bool’schen Wert festgelegt, ob ein Account aktiv ist, oder nicht. So können einzelne User temporär deaktiviert werden, ohne gelöscht werden zu müssen. &amp;ldquo;sendonly&amp;rdquo; wird auf &amp;ldquo;true&amp;rdquo; gesetzt, wenn der Account nur zum Senden von E-Mails genutzt werden soll – nicht aber zum Empfang. Das kann beispielsweise für Foren- oder Blogsoftware sinnvoll sein, die mit ihrem Account nur E-Mails verschicken soll.&lt;/p&gt;
&lt;h3 id="alias-tabelle"&gt;Alias-Tabelle&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `aliases` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`source_username` varchar(64),
`source_domain` varchar(255) NOT NULL,
`destination_username` varchar(64) NOT NULL,
`destination_domain` varchar(255) NOT NULL,
`enabled` boolean DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY (`source_username`, `source_domain`, `destination_username`, `destination_domain`),
FOREIGN KEY (`source_domain`) REFERENCES `domains` (`domain`)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Die Alias-Tabelle enthält alle Weiterleitungen / Aliase und ist eigentlich selbsterklärend. Zur temporären Deaktivierung von Weiterleitungsadressen gibt es wieder ein &amp;ldquo;enabled&amp;rdquo;-Feld.&lt;/p&gt;
&lt;p&gt;Die Datenbank wird mit Datensätzen befüllt, sobald die Server fertig konfiguriert sind. Bis dahin könnt ihr die MySQL-Kommandozeile mit &lt;code&gt;quit;&lt;/code&gt; verlassen.&lt;/p&gt;
&lt;h2 id="vmail-benutzer-und--verzeichnis-einrichten"&gt;vmail-Benutzer und -Verzeichnis einrichten&lt;/h2&gt;
&lt;p&gt;Alle Mailboxen werden direkt im Dateisystem des Debian Servers abgelegt. Für die Zugriffe auf die Mailbox-Verzeichnisse wird ein eigener Benutzer &amp;ldquo;vmail&amp;rdquo; (&amp;ldquo;Virtual Mail&amp;rdquo;) erstellt, unter dem die Zugriffe von Dovecot und anderen Komponenten des Mailservers geschehen sollen. Einerseits wird so verhindert, dass Mailserver-Komponenten auf sensible Systemverzeichnisse Zugriff bekommen, andererseits können wir so die Mailboxen vor dem Zugriff von außen schützen. Nur vmail (und root) dürfen auf die Mailboxen zugreifen.&lt;/p&gt;
&lt;p&gt;Das Verzeichnis &lt;code&gt;/var/vmail/&lt;/code&gt; soll alle Mailserver-relevanten Dateien (also Mailboxen und Filterscripts) enthalten und wird für den vmail-User als Home Directory festgelegt.&lt;/p&gt;
&lt;p&gt;vmail-Benutzer erstellen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useradd --create-home --home-dir /var/vmail --user-group --shell /usr/sbin/nologin vmail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vmail Unterverzeichnisse erstellen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /var/vmail/mailboxes
mkdir -p /var/vmail/sieve/global
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verzeichnis &lt;code&gt;/var/vmail&lt;/code&gt; rekursiv an vmail-User übereignen und Verzeichnisrechte passend setzen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chown -R vmail /var/vmail
chgrp -R vmail /var/vmail
chmod -R 770 /var/vmail
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="dovecot-installieren-und-konfigurieren"&gt;Dovecot installieren und konfigurieren&lt;/h2&gt;
&lt;p&gt;Nachdem die Datenbank und der vmail-User angelegt wurden, widmen wir uns nun dem Dovecot-Server. Wie bereits erwähnt, verwaltet dieser Server die Mailboxen und bekommt daher (in der Gestalt des vmail-Users) exklusiv Zugriff auf &lt;code&gt;/var/vmail/&lt;/code&gt;. Zuerst müssen jedoch alle Serverkomponenten installiert werden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install dovecot-core dovecot-imapd dovecot-lmtpd dovecot-mysql dovecot-sieve dovecot-managesieved
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dovecot-core&lt;/code&gt;: Dovecot-Kern&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dovecot-imapd&lt;/code&gt;: Fügt IMAP-Funktionalität hinzu&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dovecot-lmtp&lt;/code&gt;: Fügt LMTP (Local Mail Transfer Protocol)-Funktionalität hinzu; LMTP wird als MTP-Protokoll zwischen Postfix und Dovecot genutzt&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dovecot-mysql&lt;/code&gt;: Lässt Dovecot mit der MySQL-Datenbank zusammenarbeiten&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dovecot-sieve&lt;/code&gt;: Fügt Filterfunktionalität hinzu&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dovecot-managesieved&lt;/code&gt;: Stellt eine Schnittstelle zur Einrichtung der Filter via Mailclient bereit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nach der Installation wird Dovecot automatisch gestartet. Beendet Dovecot, solange wir keine fertige Konfiguration haben:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop dovecot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nun geht es an die Konfiguration. Die Dovecot-Konfigurationsdateien liegen im Verzeichnis &lt;code&gt;/etc/dovecot/&lt;/code&gt;. Dort könnt ihr schon einige Konfigurationen sehen, die bei der Installation angelegt wurden. Wir werden die Konfiguration von Grund auf neu aufsetzen - deshalb wird zuerst einmal die gesamte Dovecot-Konfiguration gelöscht:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -r /etc/dovecot/*
cd /etc/dovecot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Für Dovecot reicht die Konfigurationsdatei &lt;code&gt;/etc/dovecot/dovecot.conf&lt;/code&gt; aus. Gebt ihr über einen Editor eurer Wahl folgenden Inhalt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Dovecot config and storage versions
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
###
### Protocol settings
#############################
protocols {
lmtp = yes
imap = yes
sieve = yes
}
protocol imap {
mail_plugins {
imap_quota = yes
imap_sieve = yes
}
imap_idle_notify_interval = 29 mins
}
protocol lmtp {
mail_plugins {
sieve = yes
notify = yes
push_notification = yes
}
postmaster_address = postmaster@mydomain.tld
}
##
## TLS Config
## Quelle: https://ssl-config.mozilla.org/#server=dovecot&amp;amp;version=2.3.9&amp;amp;config=intermediate&amp;amp;openssl=1.1.1d&amp;amp;guideline=5.4
##
ssl = required
ssl_server_cert_file = /etc/acme.sh/mydomain.tld/fullchain.pem
ssl_server_key_file = /etc/acme.sh/mydomain.tld/privkey.pem
ssl_server_dh_file = /etc/dovecot/dh4096.pem
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
###
### Dovecot services
################################
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
group = postfix
user = postfix
}
}
service auth {
### Auth socket für Postfix
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
###
### SQL settings
###
sql_driver = mysql
mysql /var/run/mysqld/mysqld.sock {
user = vmail
password = mypassword
dbname = vmail
}
###
### Client authentication
#############################
auth_mechanisms = plain login
auth_username_format = %{user | lower }
passdb sql {
query = SELECT username AS user, domain, password FROM accounts WHERE username = &amp;#39;%{user | username | lower }&amp;#39; AND domain = &amp;#39;%{user | domain | lower}&amp;#39; and enabled = true;
default_password_scheme = ARGON2ID
}
userdb sql {
query = SELECT concat(quota, &amp;#39;M&amp;#39;) AS quota_storage_size FROM accounts WHERE username = &amp;#39;%{user | username | lower }&amp;#39; AND domain = &amp;#39;%{user | domain | lower}&amp;#39; AND sendonly = false;
iterate_query = SELECT username, domain FROM accounts where sendonly = false;
}
##
### Address tagging
###
recipient_delimiter = +
###
### Mail location
#######################
mail_uid = vmail
mail_gid = vmail
mail_privileged_group = vmail
mail_home = /var/vmail/mailboxes/%{user | domain }/%{user | username }
mail_driver = maildir
mail_path = ~/mail
mailbox_list_layout = fs
###
### Mailbox configuration
########################################
namespace inbox {
inbox = yes
mailbox Spam {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
quota_storage_extra = 10M
}
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Sent {
auto = subscribe
special_use = \Sent
}
}
###
### Mail plugins
############################
mail_plugins {
quota = yes
}
sieve_plugins {
sieve_imapsieve = yes
sieve_extprograms = yes
}
sieve_script spam-global {
type = before
path = /var/vmail/sieve/global/spam-global.sieve
}
sieve_script personal {
type = personal
path = /var/vmail/sieve/%{user | domain }/%{user | username }/scripts
active_path = /var/vmail/sieve/%{user | domain }/%{user | username }/active-script.sieve
}
# From Mailbox to Spam
mailbox Spam {
sieve_script spam {
type = before
cause = copy
path = /var/vmail/sieve/global/learn-spam.sieve
}
}
# From Spam to another folder (learn HAM)
imapsieve_from Spam {
sieve_script ham {
type = before
cause = copy
path = /var/vmail/sieve/global/learn-ham.sieve
}
}
# Sieve extensions only allowed in global context
sieve_global_extensions {
vnd.dovecot.pipe = yes
}
sieve_pipe_bin_dir = /usr/bin
###
### IMAP Quota
###########################
quota &amp;#34;User quota&amp;#34; {
driver = count
}
&lt;/code&gt;&lt;/pre&gt;&lt;div class="tip"&gt;
&lt;p&gt;Anzupassende Stellen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;postmaster_address&lt;/code&gt;: Domain anpassen&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_server_cert_file&lt;/code&gt;: Pfad zur Zertifikatsdatei&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_server_key_file&lt;/code&gt;: Pfad zur Zertifikatsdatei&lt;/li&gt;
&lt;li&gt;&lt;code&gt;password = mypassword&lt;/code&gt; in &lt;code&gt;mysql&lt;/code&gt; Block&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;Nach dem Speichern ist es wichtig, nur dem root-User das Lesen der Datei zu erlauben, weil das MySQL-Passwort darin in Klartext abgelegt ist:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; /etc/dovecot/dovecot.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="diffie-hellman-parameter-für-dovecot-generieren"&gt;Diffie-Hellman Parameter für Dovecot generieren&lt;/h3&gt;
&lt;p&gt;In neueren Versionen generiert Dovecot die Diffie-Hellman-Parameter für TLS-Verbindungen nicht mehr selbst. Also müssen wir selbst Hand anlegen und die Parameter mittels OpenSSL-Tool generieren lassen:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;openssl dhparam -out /etc/dovecot/dh4096.pem &lt;span style="color:#ae81ff"&gt;4096&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das kann je nach Auslastung des Mailservers durchaus eine viertel Stunde oder länger dauern. Um den Vorhang zu beschleunigen, kann ein sog. Entropy Harvester wie z.B. &lt;code&gt;haveged&lt;/code&gt; installiert werden (&lt;code&gt;apt install haveged&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id="globales-sieve-filterscript-für-spam"&gt;Globales Sieve-Filterscript für Spam&lt;/h3&gt;
&lt;p&gt;Unter &lt;code&gt;/var/vmail/sieve/global/&lt;/code&gt; wird das Sieve-Filterscript &lt;code&gt;spam-global.sieve&lt;/code&gt; erstellt, das erkannte Spammails in den Unterordner &amp;ldquo;Spam&amp;rdquo; jeder Mailbox einsortiert. Rspamd markiert erkannte E-Mails mit einem speziellen Spam-Header, den das Script erkennt. Inhalt von &lt;code&gt;spam-global.sieve&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require &amp;quot;fileinto&amp;quot;;
if header :contains &amp;quot;X-Spam-Flag&amp;quot; &amp;quot;YES&amp;quot; {
fileinto &amp;quot;Spam&amp;quot;;
}
if header :is &amp;quot;X-Spam&amp;quot; &amp;quot;Yes&amp;quot; {
fileinto &amp;quot;Spam&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="spam-learning-mit-rspamd"&gt;Spam Learning mit Rspamd&lt;/h3&gt;
&lt;p&gt;Beim Verschieben von Spammails in den Spam-Ordner bzw. dem Zurückschieben falsch einsortierter Mails in den Posteingang soll ein Lernprozess von Rspamd ausgelöst werden, sodass der Filter aus False Negatives und False Positives lernt und so mit der Zeit immer besser wird.&lt;/p&gt;
&lt;p&gt;Dazu werden zwei Sieve-Scripts in &lt;code&gt;/var/vmail/sieve/global/&lt;/code&gt; angelegt:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;learn-spam.sieve&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require [&amp;quot;vnd.dovecot.pipe&amp;quot;, &amp;quot;copy&amp;quot;, &amp;quot;imapsieve&amp;quot;];
pipe :copy &amp;quot;rspamc&amp;quot; [&amp;quot;learn_spam&amp;quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;learn-ham.sieve&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require [&amp;quot;vnd.dovecot.pipe&amp;quot;, &amp;quot;copy&amp;quot;, &amp;quot;imapsieve&amp;quot;, &amp;quot;environment&amp;quot;, &amp;quot;variables&amp;quot;];
if environment :matches &amp;quot;imap.mailbox&amp;quot; &amp;quot;*&amp;quot; {
set &amp;quot;mailbox&amp;quot; &amp;quot;${1}&amp;quot;;
}
if string &amp;quot;${mailbox}&amp;quot; &amp;quot;Trash&amp;quot; {
stop;
}
pipe :copy &amp;quot;rspamc&amp;quot; [&amp;quot;learn_ham&amp;quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Das &lt;code&gt;learn-ham.sieve&lt;/code&gt; Script besitzt noch einen Check, der überprüft, ob eine E-Mail aus dem Spam in den Papierkorb verschoben wurde. In diesem Fall sollen die betroffenen Mails ja nicht wieder als &amp;ldquo;Ham&amp;rdquo; markiert werden, daher wird in so einem Fall die Ausführung des Lernvorgangs verhindert.&lt;/p&gt;
&lt;h2 id="postfix-installieren-und-konfigurieren"&gt;Postfix installieren und konfigurieren&lt;/h2&gt;
&lt;p&gt;Für unseren Postfix-Server benötigen wir nur zwei Pakete: Das Kernpaket &lt;code&gt;postfix&lt;/code&gt; und die Komponente &lt;code&gt;postfix-mysql&lt;/code&gt;, die Postfix mit der MySQL-Datenbank kommunizieren lässt.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install postfix postfix-mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Während der Installation werdet ihr nach der &amp;ldquo;Allgemeinen Art der Konfiguration&amp;rdquo; gefragt. Wählt an dieser Stelle &amp;ldquo;Keine Konfiguration&amp;rdquo; und beendet den Postfix-Server wieder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop postfix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Im Postfix-Konfigurationsverzeichnis &lt;code&gt;/etc/postfix&lt;/code&gt; befinden sich trotz unserer Wahl &amp;ldquo;Keine Konfiguration&amp;rdquo; ein paar Konfigurationsdateien, die zunächst entfernt werden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /etc/postfix
rm -r sasl
rm master.cf main.cf.proto master.cf.proto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Legt dann folgende Dateien im Verzeichnis &lt;code&gt;/etc/postfix&lt;/code&gt; an:&lt;/p&gt;
&lt;h3 id="etcpostfixmaincf"&gt;&lt;code&gt;/etc/postfix/main.cf&lt;/code&gt;&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;##
## Netzwerkeinstellungen
##
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
inet_interfaces = 127.0.0.1, ::1, 203.0.113.1, 2001:db8::1
myhostname = mail.mysystems.tld
##
## Mail-Queue Einstellungen
##
maximal_queue_lifetime = 1h
bounce_queue_lifetime = 1h
maximal_backoff_time = 15m
minimal_backoff_time = 5m
queue_run_delay = 5m
##
## TLS Einstellungen
## Quelle: https://ssl-config.mozilla.org/#server=postfix&amp;amp;version=3.4.8&amp;amp;config=intermediate&amp;amp;openssl=1.1.1d&amp;amp;guideline=5.4
##
### Allgemein
tls_preempt_cipherlist = no
tls_ssl_options = NO_COMPRESSION
tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
### Ausgehende SMTP-Verbindungen (Postfix als Sender)
smtp_tls_security_level = dane
smtp_tls_mandatory_protocols = &amp;gt;=TLSv1.2
smtp_tls_protocols = &amp;gt;=TLSv1.2
smtp_dns_support_level = dnssec
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_ciphers = medium
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
### Eingehende SMTP-Verbindungen
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_ciphers = medium
smtpd_tls_mandatory_protocols = &amp;gt;=TLSv1.2
smtpd_tls_protocols = &amp;gt;=TLSv1.2
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtpd_tls_cert_file=/etc/acme.sh/mail.mysystems.tld/fullchain.pem
smtpd_tls_key_file=/etc/acme.sh/mail.mysystems.tld/privkey.pem
##
## Lokale Mailzustellung an Dovecot
##
virtual_transport = lmtp:unix:private/dovecot-lmtp
##
## Spamfilter und DKIM-Signaturen via Rspamd
##
smtpd_milters = inet:localhost:11332
non_smtpd_milters = inet:localhost:11332
milter_default_action = accept
##
## Server Restrictions für Clients, Empfänger und Relaying
## (im Bezug auf S2S-Verbindungen. Mailclient-Verbindungen werden in master.cf im Submission-Bereich konfiguriert)
##
### Bedingungen, damit Postfix als Relay arbeitet (für Clients)
smtpd_relay_restrictions = reject_non_fqdn_recipient
reject_unknown_recipient_domain
permit_mynetworks
reject_unauth_destination
### Bedingungen, damit Postfix ankommende E-Mails als Empfängerserver entgegennimmt (zusätzlich zu relay-Bedingungen)
### check_recipient_access prüft, ob ein account sendonly ist
smtpd_recipient_restrictions = check_recipient_access proxy:mysql:/etc/postfix/sql/recipient-access.cf
### Bedingungen, die SMTP-Clients erfüllen müssen (sendende Server)
smtpd_client_restrictions = permit_mynetworks
check_client_access hash:/etc/postfix/without_ptr
reject_unknown_client_hostname
### Wenn fremde Server eine Verbindung herstellen, müssen sie einen gültigen Hostnamen im HELO haben.
smtpd_helo_required = yes
smtpd_helo_restrictions = permit_mynetworks
reject_invalid_helo_hostname
reject_non_fqdn_helo_hostname
reject_unknown_helo_hostname
# Clients blockieren, wenn sie versuchen zu früh zu senden
smtpd_data_restrictions = reject_unauth_pipelining
##
## Restrictions für MUAs (Mail user agents)
##
mua_relay_restrictions = reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_mynetworks,permit_sasl_authenticated,reject
mua_sender_restrictions = permit_mynetworks,reject_non_fqdn_sender,reject_sender_login_mismatch,permit_sasl_authenticated,reject
mua_client_restrictions = permit_mynetworks,permit_sasl_authenticated,reject
##
## MySQL Abfragen
##
proxy_read_maps = proxy:mysql:/etc/postfix/sql/aliases.cf
proxy:mysql:/etc/postfix/sql/accounts.cf
proxy:mysql:/etc/postfix/sql/domains.cf
proxy:mysql:/etc/postfix/sql/recipient-access.cf
proxy:mysql:/etc/postfix/sql/sender-login-maps.cf
virtual_alias_maps = proxy:mysql:/etc/postfix/sql/aliases.cf
virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/accounts.cf
virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/domains.cf
local_recipient_maps = $virtual_mailbox_maps
##
## Sonstiges
##
### Maximale Größe der gesamten Mailbox (soll von Dovecot festgelegt werden, 0 = unbegrenzt)
mailbox_size_limit = 0
### Maximale Größe eingehender E-Mails in Bytes (50 MB)
message_size_limit = 52428800
### Keine System-Benachrichtigung für Benutzer bei neuer E-Mail
biff = no
### Nutzer müssen immer volle E-Mail Adresse angeben - nicht nur Hostname
append_dot_mydomain = no
### Trenn-Zeichen für &amp;#34;Address Tagging&amp;#34;
recipient_delimiter = +
### Keine Rückschlüsse auf benutzte Mailadressen zulassen
disable_vrfy_command = yes
&lt;/code&gt;&lt;/pre&gt;&lt;div class="tip"&gt;
&lt;p&gt;Anzupassen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inet_interfaces&lt;/code&gt;: IP-Adressen &lt;code&gt;203.0.113.1, 2001:db8::1&lt;/code&gt; müssen durch eigene IPv4- (und optional IPv6)-Adresse ersetzt werden.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;myhostname&lt;/code&gt;: Ersetzen durch eigenen Hostnamen&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smtpd_tls_cert_file&lt;/code&gt;: Pfad zur Zertifikatsdatei&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smtpd_tls_key_file&lt;/code&gt;: Pfad zur Zertifikatsschlüsseldatei&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="warning"&gt;
&lt;p&gt;Wenn der Mailserver (entgegen meiner Empfehlung) nicht einen &lt;code&gt;mail.domain.tld&lt;/code&gt; Hostnamen hat, sondern unter dem Domainnamen läuft (&lt;code&gt;domain.tld&lt;/code&gt;), muss zur Konfiguration eine Zeile mit&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mydestination =
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(ohne Inhalt nach dem &amp;ldquo;=&amp;rdquo;) hinzugefügt werden, sonst werden E-Mails an der falschen Stelle gespeichert.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id="etcpostfixmastercf"&gt;&lt;code&gt;/etc/postfix/master.cf&lt;/code&gt;&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
###
### SMTP-Serverbindungen aus dem Internet
### Authentifizuerung hier nicht erlaubt (Anmeldung nur via smtps/submission!)
smtp inet n - y - - smtpd
-o smtpd_sasl_auth_enable=no
###
### SMTPS Service (Submission mit implizitem TLS - ohne STARTTLS) - Port 465
### Für Mailclients gelten andere Regeln, als für andere Mailserver (siehe smtpd_ in main.cf)
###
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_sasl_security_options=noanonymous
-o smtpd_client_restrictions=$mua_client_restrictions
-o smtpd_sender_restrictions=$mua_sender_restrictions
-o smtpd_relay_restrictions=$mua_relay_restrictions
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_sender_login_maps=proxy:mysql:/etc/postfix/sql/sender-login-maps.cf
-o smtpd_helo_required=no
-o smtpd_helo_restrictions=
###
### Submission-Zugang für Clients (mit STARTTLS - für Rückwärtskompatibilität) - Port 587
###
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_sasl_security_options=noanonymous
-o smtpd_client_restrictions=$mua_client_restrictions
-o smtpd_sender_restrictions=$mua_sender_restrictions
-o smtpd_relay_restrictions=$mua_relay_restrictions
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_sender_login_maps=proxy:mysql:/etc/postfix/sql/sender-login-maps.cf
-o smtpd_helo_required=no
-o smtpd_helo_restrictions=
###
### Weitere wichtige Dienste für den Serverbetrieb
###
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
qmgr unix n - n 300 1 qmgr
tlsmgr unix - - y 1000? 1 tlsmgr
rewrite unix - - y - - trivial-rewrite
bounce unix - - y - 0 bounce
defer unix - - y - 0 bounce
trace unix - - y - 0 bounce
verify unix - - y - 1 verify
flush unix n - y 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - y - - smtp
relay unix - - y - - smtp
showq unix n - y - - showq
error unix - - y - - error
retry unix - - y - - error
discard unix - - y - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - y - - lmtp
anvil unix - - y - 1 anvil
scache unix - - y - 1 scache
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="sql-konfiguration"&gt;SQL-Konfiguration&lt;/h3&gt;
&lt;p&gt;Neben der Haupt-Konfiguration main.cf und der Service-Konfiguration master.cf werden noch ein paar Konfigurationsdateien innerhalb des Unterverzeichnisses &lt;code&gt;sql/&lt;/code&gt; angelegt, die die SQL-Queries für die Datenabfragen an die Datenbank enthalten:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /etc/postfix/sql &amp;amp;&amp;amp; cd $_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Erstellt dann die folgenden Konfigurationsdateien mit dem zugehörigen Inhalt:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;accounts.cf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user = vmail
password = vmaildbpass
hosts = unix:/run/mysqld/mysqld.sock
dbname = vmail
query = select 1 as found from accounts where username = '%u' and domain = '%d' and enabled = true LIMIT 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;aliases.cf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user = vmail
password = vmaildbpass
hosts = unix:/run/mysqld/mysqld.sock
dbname = vmail
query = SELECT DISTINCT concat(destination_username, '@', destination_domain) AS destinations FROM aliases
WHERE (source_username = '%u' OR source_username IS NULL) AND source_domain = '%d'
AND enabled = true
AND NOT EXISTS (SELECT id FROM accounts WHERE username = '%u' and domain = '%d');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;domains.cf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user = vmail
password = vmaildbpass
hosts = unix:/run/mysqld/mysqld.sock
dbname = vmail
query = SELECT domain FROM domains WHERE domain='%s';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;recipient-access.cf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user = vmail
password = vmaildbpass
hosts = unix:/run/mysqld/mysqld.sock
dbname = vmail
query = select if(sendonly = true, 'REJECT', 'OK') AS access from accounts where username = '%u' and domain = '%d' and enabled = true LIMIT 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sender-login-maps.cf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user = vmail
password = vmaildbpass
hosts = unix:/run/mysqld/mysqld.sock
dbname = vmail
query = select concat(username, '@', domain) as 'owns' from accounts where username = '%u' AND domain = '%d' and enabled = true union select
concat(destination_username, '@', destination_domain) AS 'owns' from aliases
where source_username = '%u' and source_domain = '%d' and enabled = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Vergesst nicht, &lt;code&gt;vmaildbpass&lt;/code&gt; in jeder der Dateien durch euer eigenes Passwort zu ersetzen!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Alle SQL-Konfgurationsdateien in &lt;code&gt;/etc/postfix/sql&lt;/code&gt; werden vor dem Zugriff durch unberechtigte User geschützt:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chown -R root:postfix /etc/postfix/sql
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod g+x /etc/postfix/sql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="zusätzliche-konfigurationsdatei-without_"&gt;Zusätzliche Konfigurationsdatei &lt;em&gt;without_ptr&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;Außerdem gibt es noch eine weitere Datei &lt;code&gt;/etc/postfix/without_ptr&lt;/code&gt;, die zunächst leer bleiben kann:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /etc/postfix/without_ptr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Die Datei kann später einmal Einträge wie den folgenden beinhalten:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[2001:db8::beef] OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Der Server mit der IP &lt;code&gt;2001:db8::beef&lt;/code&gt; muss dann keinen gültigen PTR-Record (Reverse DNS) mehr besitzen und wird trotzdem akzeptiert. Die &lt;code&gt;without_ptr&lt;/code&gt;-Datei muss dann nach jeder Änderung in eine Datenbankdatei umgewandelt und Postfix neu geladen werden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;postmap /etc/postfix/without_ptr
systemctl reload postfix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Im Moment reicht es aber aus, einfach nur eine leere Datenbankdatei zu generieren:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;postmap /etc/postfix/without_ptr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zum Schluss wird einmal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;newaliases
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ausgeführt, um die Alias-Datei &lt;code&gt;/etc/aliases.db&lt;/code&gt; zu generieren, die Postfix standardmäßig erwartet.&lt;/p&gt;
&lt;h2 id="rspamd-1"&gt;Rspamd&lt;/h2&gt;
&lt;p&gt;Da die Rspamd-Pakete für Debian nicht besonders aktuell sind und &lt;a href="https://rspamd.com/downloads.html#debian-standard-repos-notes"&gt;offiziell davon abgeraten wird&lt;/a&gt;, diese zu nutzen, nutzen wir an dieser Stelle das Debian-Repository des Rspamd-Projekts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Add Rspamd repository GPG key (modern method)
curl -fsSL https://rspamd.com/apt-stable/gpg.key | \
sudo gpg --dearmor -o /usr/share/keyrings/rspamd.gpg
# Add Rspamd repository
echo &amp;quot;deb [signed-by=/usr/share/keyrings/rspamd.gpg] https://rspamd.com/apt-stable/ $(lsb_release -cs) main&amp;quot; | \
sudo tee /etc/apt/sources.list.d/rspamd.list
# Update package list
sudo apt update
# Install Rspamd and Redis
sudo apt install rspamd redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neben Rspamd wird auch Redis installiert, damit Rspamd seine Daten ablegen kann (Key-Value Datenbank).&lt;/p&gt;
&lt;p&gt;Rspamd stoppen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl stop rspamd
&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src=""&gt;
&lt;/figure&gt;
&lt;h3 id="grundkonfiguration"&gt;Grundkonfiguration&lt;/h3&gt;
&lt;p&gt;Die Konfiguration von Rspamd wird im Verzeichnis &lt;code&gt;/etc/rspamd/local.d/&lt;/code&gt; abgelegt. Die folgenden Konfigurationsdateien werden darin erstellt:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/rspamd/local.d/worker-proxy.inc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Enable milter mode for Postfix integration
milter = yes;
timeout = 120s;
upstream &amp;quot;local&amp;quot; {
default = yes;
self_scan = yes; # Scan messages directly
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/etc/rspamd/local.d/worker-controller.inc&lt;/code&gt;: Einstellung des Worker controllers: Passwort für den Zugriff via Weboberfläche, z.B.:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;password = &amp;quot;$2$qecacwgrz13owkag4gqcy5y7yeqh7yh4$y6i6gn5q3538tzsn19ojchuudoauw3rzdj1z74h5us4gd3jj5e8y&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Der Passworthash (&lt;code&gt;$2$qecacwg...&lt;/code&gt;) wird via&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rspamadm pw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;generiert und in die Datei eingefügt. Vergesst nicht das abschließende &lt;code&gt;&amp;quot;;&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/rspamd/local.d/logging.inc&lt;/code&gt;: Error logging&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type = &amp;quot;syslog&amp;quot;;
level = &amp;quot;warning&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Milter Headers &lt;code&gt;/etc/rspamd/local.d/milter_headers.conf&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use = [&amp;quot;x-spam-status&amp;quot;, &amp;quot;authentication-results&amp;quot;];
authenticated_headers = [&amp;quot;authentication-results&amp;quot;];
routines {
x-spam-status {
header = &amp;quot;X-Spam-Status&amp;quot;;
remove = 0;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip;und in &lt;code&gt;/etc/rspamd/local.d/redis.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Redis connection for statistics and caching
servers = &amp;quot;127.0.0.1:6379&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Für den Bayes&amp;rsquo;schen Filter (Textanalyse) wird &amp;ldquo;Auto-Learning&amp;rdquo; aktiviert:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/rspamd/local.d/classifier-bayes.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Configure Bayes classifier to use Redis
servers = &amp;quot;127.0.0.1:6379&amp;quot;;
backend = &amp;quot;redis&amp;quot;;
# Auto-learning thresholds
autolearn = true;
min_learns = 200; # Minimum learned messages before classification
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wird eine E-Mail bereits beim Empfang abgelehnt, weil sie ausgrund der Metadaten offensichtlich von einem Spammer kommt, nutzt Rspamd die Gelegenheit und lernt, dass der Text in dieser E-Mail als Spam einzustufen ist. So wird die Erkennungsrate mit jeder Spammail besser. (&lt;a href="https://rspamd.com/doc/configuration/statistic.html"&gt;https://rspamd.com/doc/configuration/statistic.html&lt;/a&gt;)&lt;/p&gt;
&lt;h3 id="manuell-gepflegte-black-whitelists"&gt;Manuell gepflegte Black-/Whitelists&lt;/h3&gt;
&lt;p&gt;Über das Multimap-Modul kann das Verhalten von Rspamd für bestimmte E-Mail-Merkmale personalisiert gesteuert werden. Merkmale können zum Beispiel die Absender-IP-Adresse oder die Absendermailadresse sein. Für beide registrieren wir in &lt;code&gt;/etc/rspamd/local.d/multimap.conf&lt;/code&gt; jeweils eine Black- und eine Whitelist:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WHITELIST_IP {
type = &amp;quot;ip&amp;quot;;
map = &amp;quot;$CONFDIR/local.d/whitelist_ip.map&amp;quot;;
description = &amp;quot;Local ip whitelist&amp;quot;;
action = &amp;quot;accept&amp;quot;;
}
WHITELIST_FROM {
type = &amp;quot;from&amp;quot;;
map = &amp;quot;$CONFDIR/local.d/whitelist_from.map&amp;quot;;
description = &amp;quot;Local from whitelist&amp;quot;;
action = &amp;quot;accept&amp;quot;;
}
BLACKLIST_IP {
type = &amp;quot;ip&amp;quot;;
map = &amp;quot;$CONFDIR/local.d/blacklist_ip.map&amp;quot;;
description = &amp;quot;Local ip blacklist&amp;quot;;
action = &amp;quot;reject&amp;quot;;
}
BLACKLIST_FROM {
type = &amp;quot;from&amp;quot;;
map = &amp;quot;$CONFDIR/local.d/blacklist_from.map&amp;quot;;
description = &amp;quot;Local from blacklist&amp;quot;;
action = &amp;quot;reject&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Für alle vier Listen werden nun Listendateien erstellt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /etc/rspamd/local.d
touch whitelist_ip.map
touch whitelist_from.map
touch blacklist_ip.map
touch blacklist_from.map
cd -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;ldquo;IP&amp;rdquo;-Listen können IPv6- und IPv4-Adressen bzw. Adressbereiche enthalten - in jeder Zeile ein Eintrag. &amp;ldquo;From&amp;rdquo;-Listen enthalten vollständige Absendermailadressen. Die Listen können zunächst leer bleiben. Sie eignen sich gut, wenn man später einmal für einzelne Absender oder Server Ausnahmen einstellen möchte.&lt;/p&gt;
&lt;p&gt;Damit die Listendateien (.map-Dateien) auch vom Rspamd Webinterface aus bearbeitbar sind, bekommen sie nun noch die passenden Berechtigungen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chown -R _rspamd:_rspamd /etc/rspamd/local.d/*.map
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="dkim-signing"&gt;DKIM Signing&lt;/h3&gt;
&lt;p&gt;Rspamd übernimmt auch das Signieren von ausgehenden E-Mails. Damit signiert werden kann, muss zunächst ein Signing Key generiert werden. Der Parameter &lt;code&gt;-s 2026&lt;/code&gt; gibt den sogenannten Selektor an - einen Namen für den Key (hier das Erstellungsjahr).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /var/lib/rspamd/dkim/
rspamadm dkim_keygen -b 2048 -s 2026 -k /var/lib/rspamd/dkim/2026.key &amp;gt; /var/lib/rspamd/dkim/2026.txt
chown -R _rspamd:_rspamd /var/lib/rspamd/dkim
chmod 440 /var/lib/rspamd/dkim/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zum Signing Key (&lt;code&gt;/var/lib/rspamd/dkim/2026.key&lt;/code&gt;) gehört ein dazu passender Public Key, welcher in Form eines vorbereiteten DNS-Records in der Datei &lt;code&gt;/var/lib/rspamd/dkim/2026.txt&lt;/code&gt; liegt.&lt;/p&gt;
&lt;p&gt;DKIM Record ausgeben lassen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /var/lib/rspamd/dkim/2026.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Die Ausgabe sieht z.B. so aus:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2026._domainkey IN TXT ( &amp;quot;v=DKIM1; k=rsa; &amp;quot;
&amp;quot;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2/al5HqXUpe+HUazCr6t9lv2VOZLR369PPB4t+dgljZQvgUsIKoYzfS/w9NagS32xZYxi1dtlDWuRfTU/ahHO2MYzE0zHE4lMfwb6VkNCG+pM6bAkCwc5cFvyRygwxAPEiHAtmtU5b0i9LY25Z/ZWgyBxEWZ0Wf+hLjYHvnvMqewPsduUqKVjDOdUqeBb1VAu3WFErOAGVUYfKqFX&amp;quot;
&amp;quot;+yfz36Alb7/OMAort8A5Vo5t5k0vxTHzkYYg5KB6tLS8jngrNucGjyNL5+k0ijPs3yT7WpTGL3U3SEa8cX8WvOO1fIpWQz4yyZJJ1Mm62+FskSc7BHjdiMHE64Id/UBDDVjxwIDAQAB&amp;quot;
) ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Der Werte-Abschnitt des Records (beginnend mit &lt;code&gt;v=DKIM1&lt;/code&gt; bis zum Ende der Zeichenkolonne - hier &lt;code&gt;AQAB&lt;/code&gt;) muss in einen neuen DNS-Record gepflanzt werden. Dabei werden Zeilenumbrüche und Anführungsstriche entfernt. Je nach DNS-Hoster muss ein neuer Eintrag geringfügig anders formatiert werden. Ich habe den Schritt im Screenshot beispielhaft für den Hoster &lt;a href="https://www.core-networks.de/"&gt;Core-Networks.de&lt;/a&gt; mit dem Selector &amp;ldquo;2026&amp;rdquo; und meiner Beispieldomain &amp;ldquo;mysystems.tld&amp;rdquo; durchgeführt:&lt;/p&gt;
&lt;figure&gt;&lt;img src=""&gt;
&lt;/figure&gt;
&lt;p&gt;Wichtig: Der Selektor &amp;ldquo;2026&amp;rdquo; wird im DNS-Record zusammen mit &lt;code&gt;._domainkey&lt;/code&gt; als Name verwendet! Der DKIM-Record muss &lt;strong&gt;für jede verwendete Domain (domain2.tld, domain2.tld) im jeweiligen Zonefile erstellt werden!&lt;/strong&gt;&lt;/p&gt;
&lt;div class="tip"&gt;
Sollte der Record vom DNS-Hoster nicht akzeptiert werden, hilft es in einigen Fällen, die Schlüssellänge von 2048 Bit auf 1024 Bit zu reduzieren. Dazu das &lt;code&gt;rspamadm dkim_keygen&lt;/code&gt; Kommando nochmals mit &lt;code&gt;-b 1024&lt;/code&gt; statt mit &lt;code&gt;-b 2048&lt;/code&gt; ausführen.
&lt;/div&gt;
&lt;p&gt;Der erzeugte DKIM-Key und der verwendete Selektor werden in der Konfigurationsdatei &lt;code&gt;/etc/rspamd/local.d/dkim_signing.conf&lt;/code&gt; angegeben:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path = &amp;quot;/var/lib/rspamd/dkim/$selector.key&amp;quot;;
selector = &amp;quot;2026&amp;quot;;
### Enable DKIM signing for alias sender addresses
allow_username_mismatch = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Diese Konfiguration wird in die Datei &lt;code&gt;/etc/rspamd/local.d/arc.conf&lt;/code&gt; kopiert, sodass das &lt;a href="https://rspamd.com/doc/modules/arc.html"&gt;ARC-Modul&lt;/a&gt; korrekt arbeiten kann. Es greift auf dieselben Einstellungen zurück:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp -R /etc/rspamd/local.d/dkim_signing.conf /etc/rspamd/local.d/arc.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="rspamd-starten"&gt;Rspamd starten&lt;/h2&gt;
&lt;p&gt;Rspamd und Redis können nun gestartet werden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable --now redis-server
systemctl enable rspamd
systemctl start rspamd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="nginx-proxy-für-rspamd-weboberfläche"&gt;Nginx Proxy für Rspamd Weboberfläche&lt;/h2&gt;
&lt;p&gt;Für den bequemen und sicheren Zugriff auf die Rspamd-Weboberfläche kann dieser ein Nginx HTTP-Proxy vorgeschaltet werden. Nginx kümmert sich dann um die Absicherung der Verbindung via HTTPS. Wer nur selten auf die Rspamd-Weboberfläche zugreift, kann auf diesen Schritt verzichten und stattdessen auch über einen SSH-Tunnel auf das Interface zugreifen (&lt;a href="https://thomas-leister.de/mailserver-debian-trixie/#ssh-tunnel"&gt;siehe unten&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Für den Rspamd-Webproxy wird in &lt;code&gt;/etc/nginx/sites-enabled/mail.mysystems.tld&lt;/code&gt; ein &lt;code&gt;location&lt;/code&gt; Block wie folgt hinzugefügt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name mail.mysystems.tld imap.mysystems.tld smtp.mysystems.tld;
http2 on;
ssl_certificate /etc/acme.sh/mail.mysystems.tld/fullchain.pem;
ssl_certificate_key /etc/acme.sh/mail.mysystems.tld/privkey.pem;
add_header Strict-Transport-Security max-age=15768000;
location /rspamd/ {
proxy_pass http://localhost:11334/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
if ($ssl_protocol = &amp;quot;&amp;quot;) {
return 301 https://$server_name$request_uri;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anzupassen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ssl_certificate: Pfad zu Zertifikat&lt;/li&gt;
&lt;li&gt;ssl_certificate_key: Pfad zu Zertifikatsschlüssel&lt;/li&gt;
&lt;li&gt;server_name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Konfiguration testen und Nginx neu laden:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx -t
systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="weboberfläche-aufrufen"&gt;Weboberfläche aufrufen&lt;/h2&gt;
&lt;p&gt;Unter &lt;a href="https://mail.mysystems.tld/rspamd/"&gt;https://mail.mysystems.tld/rspamd/&lt;/a&gt; ist die Rspamd-Weboberfläche nun erreichbar. Als Passwort wird das weiter oben erzeugte Passwort für die Weboberfläche verwendet.&lt;/p&gt;
&lt;h2 id="via-ssh-tunnel-mit-der-weboberfläche-verbinden-alternative"&gt;Via SSH-Tunnel mit der Weboberfläche verbinden (Alternative)&lt;/h2&gt;
&lt;p&gt;Statt über Nginx kann man auch mithilfe eines SSH-Tunnels eine sichere Verbindung zur Rspamd-Weboberfläche aufbauen. Dazu benötigt man auf dem lokalen Rechner allerdings einen SSH-Client. Der SSH-Befehl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -L 8080:localhost:11334 benutzer@mail.mysystems.tld -N
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;verknüpft den Port des Webinterfaces (&lt;code&gt;11334&lt;/code&gt;) mit Port &lt;code&gt;8080&lt;/code&gt; des lokalen Systems. Im lokalen Webbrowser kann die Oberfläche nun unter http://localhost:8080 erreicht werden. Mit STRG+C wird die Verbindung wieder getrennt.&lt;/p&gt;
&lt;h2 id="domains-accounts-und-aliase-in-datenbank-definieren"&gt;Domains, Accounts und Aliase in Datenbank definieren&lt;/h2&gt;
&lt;p&gt;Bevor der Mailserver sinnvoll genutzt werden kann, müssen noch Domains und Benutzer im der MySQL-Datenbank registriert werden. Loggt euch wieder auf der MySQL-Kommandozeile ein:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… und wechselt in die vmail-Datenbank:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use vmail;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="neue-domain-anlegen"&gt;Neue Domain anlegen&lt;/h3&gt;
&lt;p&gt;Damit ein neuer Benutzeraccount oder ein neuer Alias angelegt werden kann, muss die zu nutzende Domain zuvor im Mailsystem bekannt gemacht werden. Postfix verarbeitet nur E-Mails an Domains, die in der &amp;ldquo;domains&amp;rdquo;-Tabelle eingetragen sind:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO domains (domain) VALUES ('mysystems.tld');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="neuen-e-mail-account-anlegen"&gt;Neuen E-Mail-Account anlegen&lt;/h3&gt;
&lt;p&gt;Wenn die Benutzerdomain in der Domain-Tabelle angelegt ist, kann ein neuer Benutzeraccount für den Mailserver erstellt werden. Bevor das passende SQL-Kommando eingegeben und abgeschickt wird, wird jedoch zu nächst ein Passwort-Hash des gewünschten Passworts für den Account erzeugt. Mit&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm pw -s ARGON2ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(in der Bash Shell! - Nicht in der MySQL Shell!)&lt;/strong&gt; kann ein solcher Hash erzeugt werden. Beispiel für einen ausgegebenen Hash:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$VmQU9clttB7MGvxZj+Ua8A$CULlB3rPd0GCof/tRyne6Tv4f9yvZcvm3B/AMxs91uE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dieser Hash ist für jedes Passwort einzigartig und ändert sich sogar bei jeder Generierung. Legt den Hash (von vorne bis hinten, inkl. &lt;code&gt;{ARGON2ID}&lt;/code&gt;!) am besten gleich in eurer Zwischenablage oder einem Textdokument ab - ihr benötigt ihn gleich!&lt;/p&gt;
&lt;div class="warning"&gt;
Verwendet auch an dieser Stelle möglichst nur die Zeichen 0-9, a-z und A-z. Sonderzeichen können Probleme verursachen. Wenn eure Passwörter einigermaßen lang sind, reicht das völlig aus. Sonderzeichen bringen dann ohnehin keinen nennenswerten Sicherheitsgewinn.
&lt;/div&gt;
&lt;p&gt;Das SQL-Kommando zum Erzeugen eines neuen Accounts lautet beispielsweise:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO accounts (username, domain, password, quota, enabled, sendonly) VALUES ('user1', 'mysystems.tld', '{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$VmQU9clttB7MGvxZj+Ua8A$CULlB3rPd0GCof/tRyne6Tv4f9yvZcvm3B/AMxs91uE', 2048, true, false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Folgende Werte müssen / können dabei ersetzt werden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{ARGON2ID}...&lt;/code&gt;: Der Passworthash von oben muss selbstverständlich durch den eigenen ersetzt werden.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;20248&lt;/code&gt;: In diesem Beispiel erhält der Account 2 GiB Speicher (2048 MB).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;true&lt;/code&gt;: Außerdem ist er aktiviert&lt;/li&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt;: &amp;hellip; und ist &lt;em&gt;kein&lt;/em&gt; &amp;ldquo;sendonly-Account&amp;rdquo; - er darf also auch E-Mails senden&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tipp: Legt am besten gleich den &lt;code&gt;postmaster@mysystems.tld&lt;/code&gt; Account an. Er sollte auf jedem Mailserver vorhanden sein, um den Administrator bei technischen Problemen mit dem Server erreichen zu können.&lt;/p&gt;
&lt;h3 id="neuen-alias-anlegen"&gt;Neuen Alias anlegen&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;INSERT INTO aliases (source_username, source_domain, destination_username, destination_domain, enabled) VALUES ('alias', 'mysystems.tld', 'user1', 'mysystems.tld', true);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; legt einen Alias für den Benutzer &amp;ldquo;&lt;a href="mailto:user1@mysystems.tld"&gt;user1@mysystems.tld&lt;/a&gt;&amp;rdquo; an. E-Mails an &amp;ldquo;&lt;a href="mailto:alias@mysystems.tld"&gt;alias@mysystems.tld&lt;/a&gt;&amp;rdquo; werden dann an die Mailbox dieses Users zugestellt.&lt;/p&gt;
&lt;p&gt;Als Zieladressen können auch E-Mail-Konten auf fremden Servern angegeben werden. Postfix leitet diese E-Mails dann weiter. Dabei kann es allerdings zu Problemen mit SPF-Records kommen – schließlich schickt Postfix unter bestimmten Umständen E-Mails unter fremden Domains, für die er laut SPF-Record nicht zuständig ist. SRS (Sender Rewriting Scheme) ist eine Lösung für dieses Problem. Von Erweiterungen, die schwerwiegende Design-Fehler einer Technik wie SPF korrigieren, halte ich allerdings nicht besonders viel, sodass ich euch empfehle, den SPF-Record (wie oben) auf einem &amp;ldquo;?all&amp;rdquo; gestellt zu lassen und auf Weiterleitungen zu fremden Servern möglichst zu verzichten. Aliase von lokalen Adressen auf andere lokale Adressen sind kein Problem.&lt;/p&gt;
&lt;p&gt;Wenn ein &lt;strong&gt;E-Mail-Verteiler&lt;/strong&gt; eingerichtet werden soll, werden mehrere Datensätze mit derselben Quelladresse eingerichtet, also z.B. so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;team@domain.tld =&amp;gt; user1@domain.tld
team@domain.tld =&amp;gt; user2@domain.tld
team@domain.tld =&amp;gt; user3@domain.tld
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ein &lt;strong&gt;Catch-All&lt;/strong&gt; hingegen kann erreicht werden, indem der &lt;code&gt;source_username&lt;/code&gt; auf &lt;code&gt;null&lt;/code&gt; gesetzt wird, z.B. so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;insert into aliases (source_username, source_domain, destination_username, destination_domain, enabled) values (null, 'domain1.tld', 'catchall', 'mysystems.tld', true);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ein Catch-All kann mit anderen Aliasen auf derselben Domain ko-existieren und &amp;ldquo;fängt&amp;rdquo; alle Benutzernamen auf der jeweiligen Domain auf, für die noch kein Alias gesetzt wurde. So kann man Mails zentral aufsammeln, welche an Mailboxen verschickt wurden, die auf dem Mailserver nicht existieren. Beachtet, dass dadurch das Spamaufkommen ggf. höher ist.&lt;/p&gt;
&lt;h2 id="optional-firewall-konfigurieren"&gt;Optional: Firewall konfigurieren&lt;/h2&gt;
&lt;p&gt;Wer vor / auf dem Mailserver eine Firewall betreibt, die den Zugriff auf Ports beschränkt, sollte zuerst alle Mailserver-Ports freischalten:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IMAP: 143/tcp
IMAPS: 993/tcp
Submission: 587/tcp
SMTPS: 465/tcp
SMTP: 25/tcp
ManageSieve: 4190/tcp
Web HTTP: 80/tcp
Web HTTPS: 443/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Denkt unbedingt daran, auch eure IPv6-Ports zu öffnen!&lt;/strong&gt; - Euer Mailsetup spricht IPv6! :-)
Die Ports 143 und 587 können geschlossen bleiben, wenn keine StartTLS-basierenden Verbindung genutzt werden sollen (siehe Hinweis weiter unten).&lt;/p&gt;
&lt;h2 id="start-all-the-things"&gt;Start all the things!&lt;/h2&gt;
&lt;p&gt;Euer neuer Mailserver ist jetzt fertig konfiguriert. Zeit für einen ersten Start!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start dovecot
systemctl start postfix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Redis und Rspamd wurden bereits gestartet)&lt;/p&gt;
&lt;h2 id="eine-verbindung-zum-mailserver-herstellen"&gt;Eine Verbindung zum Mailserver herstellen&lt;/h2&gt;
&lt;figure&gt;&lt;img src=""&gt;
&lt;/figure&gt;
&lt;p&gt;Eine Verbindung könnt ihr über jeden IMAP-fähigen E-Mail-Client und den folgenden Verbindungsparametern herstellen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IMAP(S)
&lt;ul&gt;
&lt;li&gt;Host: imap.mysystems.tld&lt;/li&gt;
&lt;li&gt;Port: 993&lt;/li&gt;
&lt;li&gt;Verschlüsselung: TLS&lt;/li&gt;
&lt;li&gt;(dieser Server gilt auch für domain2 und domain3, falls vorhanden!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SMTP(S)
&lt;ul&gt;
&lt;li&gt;Host: smtp.mysystems.tld&lt;/li&gt;
&lt;li&gt;Port: 465&lt;/li&gt;
&lt;li&gt;Verschlüsselung: TLS&lt;/li&gt;
&lt;li&gt;(dieser Server gilt auch für domain2 und domain3, falls vorhanden!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Managesieve (optional, zur Konfiguration der benutzerspezifischen Sieve-Filterregeln)
&lt;ul&gt;
&lt;li&gt;Host: imap.mysystems.tld&lt;/li&gt;
&lt;li&gt;Port: 4190&lt;/li&gt;
&lt;li&gt;Verschlüsselung: StartTLS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Benutzername: Die &lt;strong&gt;vollständige&lt;/strong&gt; E-Mail-Adresse, also &lt;benutzername&gt;@&amp;lt;domain.tld&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sollten Verbindungen über die IMAPS (993) und SMTPS (465) Ports nicht möglich sein, kann auch auf die StartTLS-Ports zurückgegriffen werden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IMAP-Server:
&lt;ul&gt;
&lt;li&gt;Host: imap.mysystems.tld&lt;/li&gt;
&lt;li&gt;Port: 143&lt;/li&gt;
&lt;li&gt;Verschlüsselung: StartTLS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SMTP-Server:
&lt;ul&gt;
&lt;li&gt;Host: snmtp.mysystems.tld&lt;/li&gt;
&lt;li&gt;Port: 587&lt;/li&gt;
&lt;li&gt;Verschlüsselung: StartTLS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Die IETF (Internet Engineering Task Force) empfiehlt seit 2018 allerdings, auf StartTLS Verbindungen zu verzichten und auf native TLS-Verbindungen zu setzen: &lt;a href="https://tools.ietf.org/html/rfc8314"&gt;https://tools.ietf.org/html/rfc8314&lt;/a&gt;. Siehe auch: &lt;a href="https://www.golem.de/news/verschluesselung-sicherheitsrisiko-starttls-2108-158714.html"&gt;Golem.de: &amp;ldquo;Sicherheitsrisiko STARTTLS&amp;rdquo;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="debugging"&gt;Debugging&lt;/h2&gt;
&lt;p&gt;Sollten Probleme auftreten, hilft ein Blick ins Log!&lt;/p&gt;
&lt;p&gt;Das Rspamd Log kannst du z.B. so einsehen und live mitlesen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -f -u rspamd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Postfix so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -f -u postfix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; und Dovecot so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -f -u dovecot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Falls du alle Mail-relevanten Logs einsehen willst, kannst du das Kommando auch so zusammenfassen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;journalctl -f -u rspamd -u postfix -u dovecot
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="datensicherung--backup"&gt;Datensicherung / Backup&lt;/h3&gt;
&lt;p&gt;Folgende Verzeichnisse sollten ins Backup aufgenommen werden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/postfix&lt;/code&gt; (Postfix Konfiguration)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/dovecot&lt;/code&gt; (Dovecot Konfiguration)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/rspamd&lt;/code&gt; (Rspamd Konfiguration)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/vmail&lt;/code&gt; (E-Mail Daten, Benutzerkonteneinstellungen und Sieve-Filter)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib/rspamd/dkim&lt;/code&gt; (DKIM-Schlüssel)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib/redis/&lt;/code&gt; (Rspamd-Daten)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Außerdem sollte unbedingt regelmäßig ein MySQL-Dump der &lt;code&gt;vmail&lt;/code&gt; Datenbank generiert, in eine Datei geschrieben und ins Backup aufgenommen werden, z.B:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysqldump vmail &amp;gt; /root/backups/vmail.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="das-wars-auch-schon-hat-dir-diese-anleitung-geholfen"&gt;Das war&amp;rsquo;s auch schon! Hat dir diese Anleitung geholfen?&lt;/h2&gt;
&lt;p&gt;Wenn ich dir mit dieser Anleitung helfen konnte, deinen Mailserver aufzusetzen, ist dir das vielleicht ein paar Euro wert.&lt;/p&gt;
&lt;p&gt;Wie du dir sicherlich vorstellen kannst, war es eine Menge Arbeit, diese Anleitung zu schreiben und zu erproben. Daher freue ich mich natürlich riesig über eine Motivationsstütze und finanzielle Hilfe, um Testserver und -Umgebungen bezahlen zu können, oder mir nach getaner Arbeit ein kühles Bier zu leisten.&lt;/p&gt;&lt;/p&gt;
&lt;p&gt;Wie du mir etwas zukommen lassen kannst, erfährst du hier: &lt;a href="https://thomas-leister.de/unterstuetzen/"&gt;Unterstützen&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Herzlichen Dank!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="häufige-fehler"&gt;Häufige Fehler&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Wähle eine Fehlermeldung, um mögliche Lösungen zu öffnen.&lt;/em&gt;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Das neue E-Mail System kann keine E-Mails nach extern senden&lt;/summary&gt;
&lt;p&gt;Neben Fehlermeldungen in den Postfix-Logs sollte auch geprüft werden, ob womöglich der Serverhoster den Versand blockiert. So blockiert &lt;strong&gt;Hetzner&lt;/strong&gt; beispielsweise die TCP Ports 25 und 465 ausgehend per default. Man kann diese Sperre für seinen VPS allerdings über einen Support-Request aufheben lassen. Dann funktioniert der Mailversand wieder wie gewohnt, siehe: &lt;a href="https://docs.hetzner.com/de/cloud/servers/faq/#warum-kann-ich-keine-mails-von-meinem-server-verschicken"&gt;&amp;ldquo;Hetzner FAQ: Warum kann ich keine Mails von meinem Server verschicken?&amp;rdquo;&lt;/a&gt;&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&amp;ldquo;fatal: no SASL authentication mechanisms&amp;rdquo; / &amp;ldquo;SASL: Connect to private/auth failed: No such file or directory&amp;rdquo;&lt;/summary&gt;
&lt;p&gt;Das Problem: Postfix kann Benutzer nicht authentifizieren, weil in /var/spool/postfix/private kein &amp;ldquo;auth&amp;rdquo; Socket verfügbar ist. Eigentlich sollte Dovecot diesen Socket bereitstellen. Der Fehler ist also bei Dovecot zu suchen – nicht bei Postfix. Leider hat die Erfahrung gezeigt, dass ein Vertipper nicht zwingend in der Socket-Konfiguration&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vorhanden sein muss. Es kommt immer wieder vor, dass der Fehler irgendwo anders in der Dovecot-Konfiguration liegt. Dovecot verschweigt einem dann den Fehler und erstellt den auth-Socket für Postfix nicht. Es lohnt sich in so einem Fall, noch einmal die ganze Dovecot-Konfiguration nach Fehlern abzusuchen.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&amp;ldquo;5.7.1 Service unavailable; client [1.3.3.7] blocked using zen.spamhaus.org&amp;rdquo;&lt;/summary&gt;
&lt;p&gt;Das Problem: Der Mailserver akzeptiert keine E-Mails vom Mailclient und gibt die oben genannte Fehlermeldung zurück.&lt;/p&gt;
&lt;p&gt;Das Problem lässt sich sehr einfach lösen, indem statt SMTP-Port 25 der SMTS-Port 465 oder Submission-Port 587 für den Mailversand im Mailclient konfiguriert wird. Auf Port 25 ist eine Blacklist aktiv, welche viele IP-Adressen z.B. aus DSL-Adressbereichen enthält. Diese Einschränkung gilt für die beiden anderen Ports nicht. Mailclients sollten niemals Port 25 zum Versenden nutzen.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;Mailclient verbindet sich nicht zum Server&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;Evtl. blockiert der Virenschutz die Verbindung?&lt;/li&gt;
&lt;li&gt;Blockiert die Server-Firewall die Verbindung von außen?&lt;/li&gt;
&lt;li&gt;Ist die richtige Serveradresse angegeben? (siehe Verbindungseinstellungen oben - möglicherweise hat dein E-Mail Client versucht, &amp;ldquo;schlau&amp;rdquo; zu sein und hat falsche Verbindungsdaten erraten.)&lt;/li&gt;
&lt;li&gt;Werden die richtigen Ports genutzt und ist der Verschlüsselungstyp richtig eingestellt?&lt;/li&gt;
&lt;li&gt;Wurde als Benutzernamen die vollständige E-Mail Adresse angegeben (und nicht nur der reine Benutzername)?&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="changelog-im-vergleich-zur-letzten-anleitung-mailserver-unter-debian-bullseye-v12"&gt;Changelog im Vergleich zur letzten Anleitung (&amp;ldquo;Mailserver unter Debian Bullseye v1.2&amp;rdquo;)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Dovecot&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dovecot: Umstellung auf Version 2.4 - Änderungen siehe auch: &lt;a href="https://thomas-leister.de/mailserver-migrate-config-to-dovecot-2.4-debian-trixie/"&gt;https://thomas-leister.de/mailserver-migrate-config-to-dovecot-2.4-debian-trixie/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Dovecot / IMAP: &lt;code&gt;mail_max_userip_connections&lt;/code&gt; entfernt, somit gilt default-Wert von 10 statt 50. Sollte für die meisten Setups genügen - verringert potential für Missbrauch bei geknackten Accounts.&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;quota_exceeded_message&lt;/code&gt; entfernt (Vereinfachung)&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;quota_storage_extra&lt;/code&gt; hinzugefügt für &amp;ldquo;Trash&amp;rdquo; Mailbox. Ermöglicht es, 10 MB mehr Speicher zu verwenden, wenn der Speicher eigentlich schon voll ist.&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;service lmtp&lt;/code&gt;: &lt;code&gt;mode = 0600&lt;/code&gt; statt &lt;code&gt;mode = 0660&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;service imap-login&lt;/code&gt; und &lt;code&gt;service managesieve-login&lt;/code&gt; entfernt&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;protocol lmtp&lt;/code&gt;: &lt;code&gt;quota = yes&lt;/code&gt; gesetzt&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;service auth&lt;/code&gt; &amp;gt; &lt;code&gt;auth-userdb&lt;/code&gt; gestrichen, wird nicht benötigt. Vereinfachung.&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;service imap-login&lt;/code&gt; gestrichen. Wird nicht benötigt. Vereinfachung.&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;service managesieve-login&lt;/code&gt; gestrichen. Wird nicht benötigt. Vereinfachung.&lt;/li&gt;
&lt;li&gt;Dovecot: &lt;code&gt;ARGON2ID&lt;/code&gt; als neuer Passworthash-Algorithmus. Auch als Default. SHA512-CRYPT wird noch genutzt wenn es so in der DB eingetragen ist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rspamd&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rspamd/ DNS: DKIM Key auf &amp;ldquo;2026&amp;rdquo; (statt &amp;ldquo;2023&amp;rdquo;) aktualisiert&lt;/li&gt;
&lt;li&gt;Rspamd: &lt;code&gt;milter_headers.conf&lt;/code&gt; angepasst für &lt;code&gt;x-spam-status&lt;/code&gt; für Mozilla Thunderbird hinzugefügt. &lt;code&gt;x-spam-bar&lt;/code&gt; und &lt;code&gt;x-spam-levels&lt;/code&gt; entfernt.&lt;/li&gt;
&lt;li&gt;Rspamd: Korrekte Berechtigungen für &lt;code&gt;.map&lt;/code&gt; Dateien in &lt;code&gt;/etc/rspamd/local.d/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Rspamd: &lt;code&gt;worker-proxy.inc&lt;/code&gt; eingeführt. Empfohlen von Rspamd Dokumentation.&lt;/li&gt;
&lt;li&gt;Rspamd: &lt;code&gt;/etc/rspamd/local.d/classifier-bayes.conf&lt;/code&gt; erweitert&lt;/li&gt;
&lt;li&gt;Rspamd: &lt;code&gt;/etc/rspamd/override.d/classifier-bayes.conf&lt;/code&gt; gelöscht&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Postfix&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Postfix: &lt;code&gt;smtpd_tls_dh1024_param_file&lt;/code&gt; aus &lt;code&gt;main.cf&lt;/code&gt; entfernt und DH-Datei &lt;code&gt;/etc/postfix/dh2048.pem&lt;/code&gt; entfernt. (obsolet)&lt;/li&gt;
&lt;li&gt;Postfix: Parameter &lt;code&gt;smtp_tls_mandatory_protocols&lt;/code&gt;, &lt;code&gt;smtp_tls_protocols&lt;/code&gt; hinzugefügt und &lt;code&gt;smtpd_tls_mandatory_protocols&lt;/code&gt;, &lt;code&gt;smtpd_tls_protocols&lt;/code&gt; vereinfacht.&lt;/li&gt;
&lt;li&gt;Postfix: Submission header cleanup entfernt. &lt;code&gt;-o cleanup_service_name=submission-header-cleanup&lt;/code&gt; 2x in &lt;code&gt;master.cf&lt;/code&gt; entfernt, Datei &lt;code&gt;submission-header-cleanup&lt;/code&gt; entfernt (Vereinfachung)&lt;/li&gt;
&lt;li&gt;Postfix: &lt;code&gt;milter_protocol&lt;/code&gt; entfernt. Default ist okay. (Vereinfachung)&lt;/li&gt;
&lt;li&gt;Postfix: &lt;code&gt;milter_mail_macros&lt;/code&gt; entfernt (Vereinfachung)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Sonstiges&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;acme.sh: Reload command angepasst. Bei Dovecot und Postfix &amp;ldquo;reload&amp;rdquo; statt &amp;ldquo;restart&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Thema &amp;ldquo;Autoconfig&amp;rdquo; entfernt (Vereinfachung)&lt;/li&gt;
&lt;li&gt;TLS Policies entfernt (&lt;code&gt;smtp_tls_policy_maps&lt;/code&gt;, &lt;code&gt;proxy_read_maps&lt;/code&gt;, SQL Tabelle &lt;code&gt;tlspolicies&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Nginx Webserver: &amp;ldquo;http2&amp;rdquo; aus &amp;ldquo;listen&amp;rdquo; Directive entfernt, stattdessen &amp;ldquo;http2 on;&amp;rdquo; gesetzt (neue Config-Syntax)&lt;/li&gt;
&lt;li&gt;Postfix / Dovecot: Erklärungen zu Parametern entfernt (Vereinfachung)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="changelog-zu-diesem-beitrag"&gt;Changelog zu diesem Beitrag&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;v1 (2026-02-07): Erster Release&lt;/li&gt;
&lt;li&gt;v2 (2026-02-24):
&lt;ul&gt;
&lt;li&gt;Abschnitt zur Generierung von &lt;code&gt;/etc/postfix/dh2048.pem&lt;/code&gt; entfernt - obsolet.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quota = yes&lt;/code&gt; aus Dovecot &amp;gt; &lt;code&gt;protocol lmtp&lt;/code&gt; &amp;gt; &lt;code&gt;mail_plugins&lt;/code&gt; entfernt: Redundant. Führt zu Warnung in Dovecot.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Mastodon Media Storage von Minio S3 zu SeaweedFS S3 migrieren</title><link>https://thomas-leister.de/mastodon-switch-minio-s3-seaweedfs/</link><pubDate>Fri, 23 Jan 2026 15:03:21 +0100</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/mastodon-switch-minio-s3-seaweedfs/</guid><description>&lt;p&gt;Bisher habe ich gerne den &lt;a href="https://www.min.io/"&gt;Minio S3 Server&lt;/a&gt; zusammen mit meiner &lt;a href="https://metalhead.club"&gt;Mastodon Instanz metalhead.club&lt;/a&gt; genutzt, um dort Mediendateien abzulegen. Doch seit Sommer letzten Jahres nimmt das Minio-Projekt leider einen Verlauf, der mich dazu bewegt hat, mich nach Alternativen umzusehen: Zuerst wurde der interne Dateibrowser aus der Verwaltungsoberfläche gestrichen - kurz darauf wurde dann verkündet, dass Minio nicht mehr als Open Source Projekt weiter gepflegt würde. Außerdem wurde die Verteilung von Minio-Binaries eingestellt. Es gab also keine Updates mehr. Minio ist seitdem effektiv tot - zumindest, was die Community Version angeht.&lt;/p&gt;
&lt;p&gt;Mit &lt;a href="https://github.com/seaweedfs/seaweedfs"&gt;SeaweedFS&lt;/a&gt; habe ich allerdings eine tolle Alternative gefunden, auf die ich meine Mastodon Instanz kürzlich umgestellt habe. Hier beschreibe ich, in welche Probleme ich dabei gelaufen bin und wie ich diese gelöst habe.&lt;/p&gt;
&lt;h2 id="seaweedfs-installieren"&gt;SeaweedFS installieren&lt;/h2&gt;
&lt;p&gt;Die Installation gestaltet sich sehr einfach: SeaweedFS besteht nur aus einem einzelnen Binary, welches heruntergeladen und unter &lt;code&gt;/usr/local/bin&lt;/code&gt; abgelegt wird:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -L https://github.com/seaweedfs/seaweedfs/releases/download/4.07/linux_amd64_full.tar.gz | tar xvz -C /usr/local/bin/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anschließend wird ein neuer SeaweedFS Systembenutzer erstellt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;useradd --system seaweedfs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;hellip; und ein passender Systemd Service angelegt:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/systemd/system/seaweedfs.ervice&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Unit]
Description=SeaweedFS Server
After=network.target
[Service]
Type=simple
User=seaweedfs
Group=seaweedfs
ExecStart=/usr/local/bin/weed server -s3 -ip 127.0.0.1 -ip.bind=127.0.0.1 -volume.max=0 -dir /mnt/s3storage1/seaweedfs
WorkingDirectory=/usr/local/bin
SyslogIdentifier=seaweedfs
[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/local/bin/weed server -s3&lt;/code&gt; startet das SeaweedFS Binary &lt;code&gt;weed&lt;/code&gt; - und damit die Komponenten &lt;code&gt;master server&lt;/code&gt;, &lt;code&gt;volume server&lt;/code&gt;, &lt;code&gt;filer&lt;/code&gt; und das &lt;code&gt;s3&lt;/code&gt; Frontend.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-ip 127.0.0.1 -ip.bind=127.0.0.1&lt;/code&gt;: Interne Kommunikationsschnittstellen und auch die S3 API sollen &lt;code&gt;localhost&lt;/code&gt;-intern bleiben, da wir SeaweedFS nicht in einem Cluster betreiben und auch die S3 Schnittstelle nur dem internen Nginx Proxy zugänglich gemacht werden soll.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-volume.max=0&lt;/code&gt; sorgt dafür, dass die Anzahl der Storage-Volumes nicht begrenzt wird. Hier könnte man ansonsten einstellen, wie viel Speicher SeaweedFS maximal nutzen darf (default: 1 Volume = 30 GB). In meinem Fall soll der gesamte zur Verfügung stehende Speicher genutzt werden.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-dir /mnt/s3storage1/seaweedfs&lt;/code&gt; hier werden die Volumes in Form von Dateien abgelegt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Service aktivieren und starten:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;systemctl enable --now seaweedfs.service
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nach einigen Sekunden sollte SeaweedFS gestartet sein. Siehe auch:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl status seaweedfs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="ein-bucket-anlegen"&gt;Ein Bucket anlegen&lt;/h2&gt;
&lt;p&gt;Über die &amp;ldquo;weed shell&amp;rdquo; kann nun ein neues S3 Bucket angelegt und konfiguriert werden. In meinem Fall heißt das neue Bucket &amp;ldquo;metalheadclub-media&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Shell öffnen:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;weed shell
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bucket erstellen:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s3.bucket.create -name metalheadclub-media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Berechtigungen setzen (dynamisch, anstatt eines json config files):&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s3.configure -access_key&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ACCESS_KEY -secret_key&lt;span style="color:#f92672"&gt;=&lt;/span&gt;SECRET_KEY -buckets&lt;span style="color:#f92672"&gt;=&lt;/span&gt;metalheadclub-media -user&lt;span style="color:#f92672"&gt;=&lt;/span&gt;mastodon -actions&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Read,Write,List -apply
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dabei wird zur Bestätigung folgendes ausgegeben:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
&amp;#34;identities&amp;#34;: [
{
&amp;#34;name&amp;#34;: &amp;#34;mastodon&amp;#34;,
&amp;#34;credentials&amp;#34;: [
{
&amp;#34;accessKey&amp;#34;: &amp;#34;ACCESS_KEY&amp;#34;,
&amp;#34;secretKey&amp;#34;: &amp;#34;SECRET_KEY&amp;#34;,
&amp;#34;status&amp;#34;: &amp;#34;&amp;#34;
}
],
&amp;#34;actions&amp;#34;: [
&amp;#34;Read:metalheadclub-media&amp;#34;,
&amp;#34;Write:metalheadclub-media&amp;#34;,
&amp;#34;List:metalheadclub-media&amp;#34;
],
&amp;#34;account&amp;#34;: null,
&amp;#34;disabled&amp;#34;: false,
&amp;#34;serviceAccountIds&amp;#34;: [],
&amp;#34;policyNames&amp;#34;: []
}
],
&amp;#34;accounts&amp;#34;: [],
&amp;#34;serviceAccounts&amp;#34;: []
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Anonyme (öffentliche) User bekommen nur Lesezugriff auf die Mediendateien:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;s3.configure -buckets&lt;span style="color:#f92672"&gt;=&lt;/span&gt;metalheadclub-media -user&lt;span style="color:#f92672"&gt;=&lt;/span&gt;anonymous -actions&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Read -apply
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ausgabe:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;{
&amp;#34;identities&amp;#34;: [
{
&amp;#34;name&amp;#34;: &amp;#34;mastodon&amp;#34;,
&amp;#34;credentials&amp;#34;: [
{
&amp;#34;accessKey&amp;#34;: &amp;#34;ACCESS_KEY&amp;#34;,
&amp;#34;secretKey&amp;#34;: &amp;#34;SECRET_KEY&amp;#34;,
&amp;#34;status&amp;#34;: &amp;#34;&amp;#34;
}
],
&amp;#34;actions&amp;#34;: [
&amp;#34;Read:metalheadclub-media&amp;#34;,
&amp;#34;Write:metalheadclub-media&amp;#34;,
&amp;#34;List:metalheadclub-media&amp;#34;
],
&amp;#34;account&amp;#34;: null,
&amp;#34;disabled&amp;#34;: false,
&amp;#34;serviceAccountIds&amp;#34;: [],
&amp;#34;policyNames&amp;#34;: []
},
{
&amp;#34;name&amp;#34;: &amp;#34;anonymous&amp;#34;,
&amp;#34;credentials&amp;#34;: [],
&amp;#34;actions&amp;#34;: [
&amp;#34;Read:metalheadclub-media&amp;#34;
],
&amp;#34;account&amp;#34;: null,
&amp;#34;disabled&amp;#34;: false,
&amp;#34;serviceAccountIds&amp;#34;: [],
&amp;#34;policyNames&amp;#34;: []
}
],
&amp;#34;accounts&amp;#34;: [],
&amp;#34;serviceAccounts&amp;#34;: []
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Anschließend kann die Shell mit &amp;ldquo;quit&amp;rdquo; wieder verlassen werden.&lt;/p&gt;
&lt;h2 id="migration-der-daten-von-minio-zu-seaweedfs"&gt;Migration der Daten von Minio zu SeaweedFS&lt;/h2&gt;
&lt;p&gt;Der SeaweedFS Server ist nun aktiv, das Bucket erstellt und mit den nötigen Berechtigungen ausgestattet - Zeit für die Migration der Daten von Minio zu SeaweedFS!&lt;/p&gt;
&lt;p&gt;Für die Migration gab es zwei Dinge zu beachten:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Die Migration sollte so &amp;ldquo;lautlos&amp;rdquo; wie möglich passieren und für die Nutzer meiner Mastodon Instanz keinen Einfluss haben.&lt;/li&gt;
&lt;li&gt;Da ich dieselbe virtuelle Maschine mit demselben Speicher für SeaweedFS nutzen wollte und der Speicher begrenzt war, sollten die Daten von Minio zu SeaweedFS &lt;em&gt;verschoben&lt;/em&gt; werden - nicht &lt;em&gt;kopiert&lt;/em&gt;!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Die Strategie lautete - wie bei vorherigen Migrationen auch schon - wie folgt:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Neues Backend in Betrieb nehmen&lt;/li&gt;
&lt;li&gt;Frontend-Proxy mit beiden Backends konfigurieren =&amp;gt; Frontend fragt zuerst neues Backend an, im Fehlerfall Fallback auf altes Backend&lt;/li&gt;
&lt;li&gt;Mastodon-Instanz umkonfigurieren: Speichert neue Medien in neues Backend, während alte Medien aus dem alten Backend zugreifbar bleiben&lt;/li&gt;
&lt;li&gt;Medien werden von altem Storage im Hintergrund langsam zu neuem Storage verschoben&lt;/li&gt;
&lt;li&gt;&amp;hellip; bis alter Storage schließlich leer ist und entfernt werden kann&lt;/li&gt;
&lt;li&gt;Fertig!&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="zwei-s3-backends-für-einen-glatten-übergang"&gt;Zwei S3 Backends für einen glatten Übergang&lt;/h3&gt;
&lt;p&gt;Konfiguration für die Datenmigration habe ich hier visualisiert:&lt;/p&gt;
&lt;figure class="diagram"&gt;&lt;img src="https://thomas-leister.de/mastodon-switch-minio-s3-seaweedfs/images/minio-seaweedfs-migration.drawio.svg"
alt="Grafik zeigt die Abhängigkeiten zwischen den Nginx Proxies und den S3 Servern. Den Start bildet ein media.metalhead.club Server, der das Frontend bildet. Dahinter führt ein Pfad direkt zu Minio - ein anderer Pfad über einen weiteren Proxy s3.650thz.de zum SeaweedFS Backend."&gt;&lt;figcaption&gt;
&lt;p&gt;Grafik zeigt die Abhängigkeiten zwischen den Nginx Proxies und den S3 Servern. Den Start bildet ein media.metalhead.club Server, der das Frontend bildet. Dahinter führt ein Pfad direkt zu Minio - ein anderer Pfad über einen weiteren Proxy s3.650thz.de zum SeaweedFS Backend.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Die obere Reihe (media.metalhead.club, Minio Server) existierte schon früher - neu hinzugekommen ist das SeaweedFS Backend und der s3.650thz.de Server. Der media.metalhead.club Nginx-Proxy sollte (ähnlich wie bei der &lt;a href="https://thomas-leister.de/en/switching-mastodon-from-scaleway-to-selfhosted-minio-s3/"&gt;Migration von Scaleway S3 zu Minio&lt;/a&gt;) wieder eine besondere Rolle einnehmen und während der Migration der Daten flexibel zwischen den beiden S3 Backends umschalten. Primär sollte das neue SeaweedFS Backend Anfragen beantworten. Für den Fall, dass das neue Backend die Daten nicht liefern konnte, wurde ein Fallback implementiert, dass dafür sorgte, dass die Anfrage dann vom alten Minio-Backend beantwortet werden konnte.&lt;/p&gt;
&lt;p&gt;Der Schlüssel liegt in diesen Zeilen:&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# During data migration: Forward request to @minio if @seaweedfs returns 404 or 403 (access denied)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;proxy_intercept_errors&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;recursive_error_pages&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;error_page&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;404&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;403&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;@minio&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="virtual-host-style-addressing-für-seaweedfs-s3"&gt;Virtual Host style addressing für SeaweedFS S3&lt;/h3&gt;
&lt;p&gt;Ein wichtiger Unterschied in der Behandlung des SeaweedFS Backend im Vergleich zu Minio ist, dass SeaweedFS &lt;em&gt;kein&lt;/em&gt; Virtual Host Addressing von S3-Buckets unterstützt! Mit Minio lässt sich ein Bucket auf zwei Arten adressieren:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bucket.s3server.tld&lt;/code&gt; oder&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s3server.tld/bucket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ersteres ist bei SeaweedFS also nicht möglich. Stattdessen &lt;a href="https://github.com/seaweedfs/seaweedfs/wiki/S3-Nginx-Proxy#example-nginx-config"&gt;schlägt das SeaweedFS Wiki vor&lt;/a&gt;, Adressen so in letztere Form umzuschreiben:&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;server_name&lt;/span&gt; ~&lt;span style="color:#e6db74"&gt;^(?:(?&amp;lt;bucket&amp;gt;[^.]+)\.)?s3\.yourdomain\.com;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;...&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 bucket subdomain is not empty,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# rewrite request to backend.
&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&lt;/span&gt; &lt;span style="color:#e6db74"&gt;(&lt;/span&gt;$bucket &lt;span style="color:#e6db74"&gt;!=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rewrite&lt;/span&gt; &lt;span style="color:#e6db74"&gt;(.*)&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$bucket$1 &lt;span style="color:#e6db74"&gt;last&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Art der Konfiguration konnte ich in meinem media.metalhead.club VirtualHost allerdings nicht umsetzen, weil das zum einem Fehler führte:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[error] 1388933#1388933: *5700433 rewrite or internal redirection cycle while redirect to named location &amp;#34;@seaweedfs&amp;#34;, client: xxx, server: media.metalhead.club, request: &amp;#34;GET /cache/custom_emojis/images/xxx.png HTTP/2.0&amp;#34;, host: &amp;#34;media.metalhead.club&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also habe ich diesen Mechanismus kurzerhand in einen weiteren vHost &amp;ldquo;s3.650thz.de&amp;rdquo; ausgelagert:&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;upstream&lt;/span&gt; &lt;span style="color:#e6db74"&gt;seaweedfs&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hash&lt;/span&gt; $arg_uploadId &lt;span style="color:#e6db74"&gt;consistent&lt;/span&gt;;
&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; 127.0.0.1:&lt;span style="color:#ae81ff"&gt;8333&lt;/span&gt; &lt;span style="color:#e6db74"&gt;fail_timeout=0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;keepalive&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&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; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;80&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#e6db74"&gt;[::]:80&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;443&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ssl&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#e6db74"&gt;[::]:443&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ssl&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enable http2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;http2&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;server_name&lt;/span&gt; ~&lt;span style="color:#e6db74"&gt;^(?&amp;lt;bucket&amp;gt;[^.]+)\.s3\.650thz\.de&lt;/span&gt; &lt;span style="color:#e6db74"&gt;s3.650thz.de&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ssl_certificate&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/etc/acme.sh/s3.650thz.de/fullchain.pem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ssl_certificate_key&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/etc/acme.sh/s3.650thz.de/privkey.pem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ignore_invalid_headers&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;client_max_body_size&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffering&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_request_buffering&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; $http_host;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Real-IP&lt;/span&gt; $remote_addr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-For&lt;/span&gt; $proxy_add_x_forwarded_for;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-Proto&lt;/span&gt; $scheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_http_version&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;chunked_transfer_encoding&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;if&lt;/span&gt; &lt;span style="color:#e6db74"&gt;(&lt;/span&gt;$bucket &lt;span style="color:#e6db74"&gt;!=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rewrite&lt;/span&gt; &lt;span style="color:#e6db74"&gt;^/(.*)&lt;/span&gt;$ &lt;span style="color:#e6db74"&gt;/&lt;/span&gt;$bucket/$1 &lt;span style="color:#e6db74"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http://seaweedfs&lt;/span&gt;; &lt;span style="color:#75715e"&gt;# This uses the upstream directive definition to load balance
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Letztendlich dient der vHost also nur der Umwandlung von einem &amp;ldquo;virtual host style addressing&amp;rdquo; in das herkömmliche &amp;ldquo;path style addressing&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Das &amp;ldquo;vHost style addressing&amp;rdquo; im media.metalhead.club vHost für die &lt;code&gt;proxy_pass&lt;/code&gt; Direktiven benötigt, da hier keine Pfade a la &lt;code&gt;proxy_pass http://seaweedfs/bucket&lt;/code&gt; angegeben werden können.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Meine media.metalhead.club für die Migrationsphase sieht nun so aus:&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;upstream&lt;/span&gt; &lt;span style="color:#e6db74"&gt;minio&lt;/span&gt; {
&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; 127.0.0.1:&lt;span style="color:#ae81ff"&gt;9000&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;keepalive&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&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; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;80&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#e6db74"&gt;[::]:80&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;443&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ssl&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;listen&lt;/span&gt; &lt;span style="color:#e6db74"&gt;[::]:443&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ssl&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;server_name&lt;/span&gt; &lt;span style="color:#e6db74"&gt;media.metalhead.club&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enable http2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;http2&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# SSL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ssl_certificate&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/etc/acme.sh/media.metalhead.club/fullchain.pem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ssl_certificate_key&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/etc/acme.sh/media.metalhead.club/privkey.pem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;client_max_body_size&lt;/span&gt; &lt;span style="color:#e6db74"&gt;100M&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;root&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/var/www/media.metalhead.club&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;access_log&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;try_files&lt;/span&gt; $uri &lt;span style="color:#e6db74"&gt;@seaweedfs&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Display index.html if user is browsing https://media.metalhead.club
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;/&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;alias&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/var/www/media.metalhead.club/&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Own SeaweedFS S3 server (new)
&lt;/span&gt;&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;@seaweedfs&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Set headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;metalheadclub-media.s3.650thz.de&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Authorization&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Hide headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Methods&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Headers&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Hide amazon S3 headers, which are sent by Minio for compatibility
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-id-2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-request-id&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-bucket-region&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amzn-requestid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-mtime&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ignore headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_ignore_headers&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Pass headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Date&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Last-Modified&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ETag&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Cache-Control&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Expires&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Connection to backend server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http://[::1]&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span style="color:#e6db74"&gt;1s&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_send_timeout&lt;/span&gt; &lt;span style="color:#e6db74"&gt;30s&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_read_timeout&lt;/span&gt; &lt;span style="color:#e6db74"&gt;30s&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Buffer settings
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_request_buffering&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffering&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffers&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;64k&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_busy_buffers_size&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;128k&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffer_size&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;32k&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Migration Workaround: Forward request to @minio if @seaweedfs returns 404 or 403 (access denied)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_intercept_errors&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;recursive_error_pages&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;error_page&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;404&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;403&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;@minio&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Cache-Status&lt;/span&gt; $upstream_cache_status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Content-Security-Policy&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;default-src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;; &lt;span style="color:#f92672"&gt;form-action&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Served-By&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;s3.650thz.de&lt;/span&gt; &lt;span style="color:#e6db74"&gt;SeaweedFS&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Own Minio S3 server (old)
&lt;/span&gt;&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;@minio&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;limit_except&lt;/span&gt; &lt;span style="color:#e6db74"&gt;GET&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;deny&lt;/span&gt; &lt;span style="color:#e6db74"&gt;all&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Set headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;metalheadclub-media.s3.650thz.de&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Authorization&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Hide headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Methods&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Headers&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Hide amazon S3 headers, which are sent by Minio for compatibility
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-id-2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-request-id&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-bucket-region&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amzn-requestid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-mtime&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ignore headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_ignore_headers&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Pass headers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Date&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Last-Modified&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;ETag&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Cache-Control&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Expires&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Connection to backend server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http://minio&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span style="color:#e6db74"&gt;1s&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_send_timeout&lt;/span&gt; &lt;span style="color:#e6db74"&gt;30s&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_read_timeout&lt;/span&gt; &lt;span style="color:#e6db74"&gt;30s&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Buffer settings
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_request_buffering&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffering&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffers&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;64k&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_busy_buffers_size&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;128k&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_buffer_size&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;32k&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Cache-Status&lt;/span&gt; $upstream_cache_status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Content-Security-Policy&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;default-src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;; &lt;span style="color:#f92672"&gt;form-action&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Served-By&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;s3.650thz.de&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Minio&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Über &lt;code&gt;add_header X-Served-By &amp;quot;s3.650thz.de ...&amp;quot;&lt;/code&gt; lasse ich mir zudem in die HTTP-header schreiben, ob eine Datei von Minio oder von SeaweedFS ausgeliefert wurde. So kann ich später prüfen, ob die Datenmigration wie erhofft funktioniert hat.&lt;/p&gt;
&lt;h3 id="umstellung-der-mastodon-instanz"&gt;Umstellung der Mastodon Instanz&lt;/h3&gt;
&lt;p&gt;Damit Mastodon die neuen Mediendateien in SeaweedFS ablegte, mussten noch die korrekten Parameter in der &lt;code&gt;.env.production&lt;/code&gt; Konfiguration gesetzt werden:&lt;/p&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_ENABLED&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_BUCKET&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;metalheadclub-media&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ACCESS_KEY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;SECRET_KEY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_ALIAS_HOST&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;media.metalhead.club&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_REGION&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;hyper2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_PROTOCOL&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;https&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_HOSTNAME&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;media.metalhead.club&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_ENDPOINT&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;https://s3.650thz.de&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_SIGNATURE_VERSION&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach einem Neustart aller Mastodon Services war die Änderung aktiv:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd /etc/systemd/system
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl restart mastodon*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ob meine Änderung erfolgreich war, konnte ich einfach prüfen: Ich wartete in der globalen Mastodon Timeline neue Posts mit Bildern ab und öffnete die Grafik in einem neuen Tab. Über die Firefox Entwicklertools lassen sich dann auch die HTTP-Header zur Datei anzeigen. Wie erhofft, war der &lt;code&gt;X-Served-By&lt;/code&gt; Header auf &amp;ldquo;SeaweedFS&amp;rdquo; gesetzt. :)&lt;/p&gt;
&lt;p&gt;&amp;hellip; und auch die alten Grafiken ließen sich nach wie vor abrufen. Sie wurden mit &lt;code&gt;X-Served-By&lt;/code&gt; &amp;ldquo;Minio&amp;rdquo; ausgeliefert.&lt;/p&gt;
&lt;h3 id="datentransfer"&gt;Datentransfer&lt;/h3&gt;
&lt;p&gt;Prima! Dann konnte es ja jetzt an die eigentliche Datenmigration gehen. Weg von Minio - hin zu SeaweedFS.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Eigentlich&lt;/em&gt; ist eine solche Migration keine hohe Kunst. Ich habe &lt;code&gt;rclone&lt;/code&gt; mit beiden S3 Buckets konfiguriert:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[minio]
type = s3
provider = Minio
env_auth = false
access_key_id = ACCESS_KEY
secret_access_key = SECRET_KEY
region = hyper2
endpoint = http://127.0.0.1:9000
location_constraint = hyper2
acl = public-read
[seaweedfs]
type = s3
provider = SeaweedFS
access_key_id = ACCESS_KEY
secret_access_key = SECRET_KEY
endpoint = http://127.0.0.1:8333
acl = public-read
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Den Zugriff auf beide Buckets habe ich mit einer einfachen Dateiauflistung getestet:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone lsd seaweedfs:metalheadclub-media
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone lsd minio:metalheadclub-media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Daraufhin habe ich begonnen, testweise zunächst nur kleinere Verzeichnisse wie z.B. &lt;code&gt;site_uploads&lt;/code&gt; von Minio zu SeaweedFS zu &lt;em&gt;kopieren&lt;/em&gt; (statt zu verschieben!):&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone sync minio:metalheadclub-media/site_uploads/ seaweedfs:metalheadclub-media/site_uploads/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wieder habe ich über die Browser-Tools getestet, wie die betroffenen Grafiken (z.B. Banner-Grafik unter &lt;a href="https://metalhead.club/about"&gt;https://metalhead.club/about&lt;/a&gt;) ausgeliefert wurden. Während sie erst von Minio ausgeliefert wurden, wurden sie nach dem &lt;code&gt;rclone sync&lt;/code&gt; von SeaweedFS ausgeliefert.&lt;/p&gt;
&lt;p&gt;Nachdem auch dieser Test erfolgreich war, migrierte ich die Mastodon Mediendaten Stück für Stück zu SeaweedFS (diesmal durch &lt;em&gt;verschieben&lt;/em&gt; mit &lt;code&gt;move&lt;/code&gt; statt mit &lt;code&gt;sync&lt;/code&gt; zu kopieren!)&lt;/p&gt;
&lt;p&gt;Liste der zu verschiebenden Verzeichnisse:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@s3 /root # rclone lsd minio:metalheadclub-media/
0 2026-01-16 14:01:17 -1 accounts
0 2026-01-16 14:01:17 -1 backups
0 2026-01-16 14:01:17 -1 cache
0 2026-01-16 14:01:17 -1 custom_emojis
0 2026-01-16 14:01:17 -1 imports
0 2026-01-16 14:01:17 -1 media_attachments
0 2026-01-16 14:01:17 -1 site_uploads
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Meine Kommandos zu verschieben:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rclone move --delete-empty-src-dirs minio:metalheadclub-media/accounts seaweedfs:metalheadclub-media/accounts
rclone move --delete-empty-src-dirs minio:metalheadclub-media/backups seaweedfs:metalheadclub-media/backups
rclone move --delete-empty-src-dirs minio:metalheadclub-media/custom_emojis seaweedfs:metalheadclub-media/custom_emojis
rclone move --delete-empty-src-dirs minio:metalheadclub-media/imports seaweedfs:metalheadclub-media/imports
rclone move --delete-empty-src-dirs minio:metalheadclub-media/media_attachments seaweedfs:metalheadclub-media/media_attachments
rclone move --delete-empty-src-dirs minio:metalheadclub-media/site_uploads seaweedfs:metalheadclub-media/site_uploads
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Wer gut aufpasst, wird bemerken, dass ich ein Verzeichnis ausgelassen habe: Das Mediencacheverzeichnis &lt;code&gt;/cache&lt;/code&gt;! Dieses ist mit Abstand das Größte und bekommt eine Sonderbehandlung &amp;hellip;&lt;/p&gt;
&lt;h3 id="ärger-mit-dem-großen-cache-verzeichnis"&gt;Ärger mit dem großen /cache Verzeichnis&lt;/h3&gt;
&lt;p&gt;Dieses Verzeichnis enthält alle Dateien, die nicht vom eigenen Mastodon-Server stammen. Also zwischengespeicherte Remote-Mediendaten und Vorschaubildchen. Je nach Cache-Vorhaltezeit kann das Verzeichnis sehr groß werden. Im Fall von metalhead.club hatte ich es hier mit etwa 700 GB und ca 3 Millionen kleinen Dateien zu tun. Tatsächlich ist nicht die Gesamtgröße das Problem, sondern die Anzahl der Dateien.&lt;/p&gt;
&lt;p&gt;Natürlich habe ich auch versucht, dieses Verzeichnis auf den neuen Storage zu bewegen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone move --delete-empty-src-dirs minio:metalheadclub-media/cache seaweedfs:metalheadclub-media/cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; doch das scheitere nach wenigen Minuten an zu wenig RAM. Ich gab mehr RAM (ca 16 GB), doch erneut war das &lt;code&gt;rclone&lt;/code&gt; Tool so RAM-hungrig, dass es vom OOM-Killer beendet wurde.&lt;/p&gt;
&lt;p&gt;Also gut. Wenn ich schon nicht alle Daten aus dem Cache Verzeichnis verschieben konnte, konnte ich sie doch einfach im alten Minio Bucket liegen lassen, bis sie aufgelaufen waren! Denn im &lt;code&gt;/cache&lt;/code&gt; Verzeichnis befinden sich ausschließlich Dateien, die für den Betrieb nicht sehr wichtig sind, und die normalerweise von Mastodon nach einer festgelegten Dauer sowieso aufgeräumt werden (Bei metalhead.club: Cache Retention 15 Tage).&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Einfach liegen lassen und warten&amp;rdquo; klingt nach einer verlockenden Strategie, aber so einfach ist es nicht. Denn durch die Umstellung der Mastodon-Software auf das neue Bucket funktionieren Mastodon-interne Löschroutinen nicht mehr. Das Bucket bleibt so voll, wie es zuletzt war. Und neue Dateien drängen auf das System. Das Ergebnis: Der Speicherverbrauch steigt.&lt;/p&gt;
&lt;p&gt;Wer sehr viel freien Speicher hat, kann möglicherweise tatsächlich einfach warten, bis 15 Tage um sind, und kann das Bucket dann als gesamtes einfach löschen. Ich hatte allerdings nur ca 170 GB freien Speicher. Der würde durch neu hinzukommende Medien im neuen Bucket schon deutlich vor 15 Tagen aufgebraucht sein. Abwarten war also keine Option. Die alten Medien mussten unbedingt nach und nach gelöscht werden.&lt;/p&gt;
&lt;p&gt;Zunächst habe ich es über rclone Kommandos versucht. Dieses Kommando löscht alle Dateien, die älter als 15 Tage sind:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone -v --min-age 15d delete --rmdirs minio:metalheadclub-media/cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; - eigentlich. Erneut waren die Anforderungen an den RAM zu hoch (z.T. 15 GB Verbrauch nur durch &lt;code&gt;rclone&lt;/code&gt;!)&lt;/p&gt;
&lt;p&gt;Mit einigen Optimierungen kann man den Verbrauch deutlich drücken:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GOGC=20 rclone --list-cutoff 100000 --min-age 15d delete --rmdirs minio:metalheadclub-media/cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Siehe auch: &lt;a href="https://rclone.org/faq/#rclone-is-using-too-much-memory-or-appears-to-have-a-memory-leak"&gt;https://rclone.org/faq/#rclone-is-using-too-much-memory-or-appears-to-have-a-memory-leak&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Als ich gerade glaubte, das Problem gelöst zu haben, kam nun ein anderer Fehler um die Ecke:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ERROR : Attempt 1/3 failed with 1 errors and: operation error S3: ListObjectsV2, exceeded maximum number of attempts, 10, https response error StatusCode: 0, RequestID: , HostID: , request send failed, Get &amp;quot;http://127.0.0.1:9000/metalheadclub-media?delimiter=&amp;amp;encoding-type=url&amp;amp;list-type=2&amp;amp;max-keys=1000&amp;amp;prefix=cache%2F&amp;quot;: net/http: timeout awaiting response headers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Offenbar dauerte das Auflisten der Dateien so lange, dass der S3 Server selbst in ein Timeout lief (nach ca. 10 Minuten). Wie sollte ich das lösen? In dem Bucket waren einfach zu viele Dateien.&lt;/p&gt;
&lt;p&gt;Auch mit dem Minio-eigenen Tool &lt;code&gt;mc&lt;/code&gt; hatte ich kein Glück:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./mc rm --recursive --force --older-than 15d minio/metalheadclub-media/cache
mc: &amp;lt;ERROR&amp;gt; Failed to remove `minio/metalheadclub-media/cache` recursively. Get &amp;quot;http://localhost:9000/metalheadclub-media/?delimiter=&amp;amp;encoding-type=url&amp;amp;fetch-owner=true&amp;amp;list-type=2&amp;amp;prefix=cache&amp;quot;: read tcp [::1]:53662-&amp;gt;[::1]:9000: i/o timeout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wie konnte ich 3 Mio. kleine Dateien loswerden? Wie sich herausstellte: Eigentlich sehr einfach &amp;hellip;&lt;/p&gt;
&lt;h3 id="dateien-löschen-über-s3-lifecycle-rules"&gt;Dateien löschen über S3 Lifecycle Rules&lt;/h3&gt;
&lt;p&gt;&amp;hellip; denn ich hatte einen praktischen S3 Mechanismus total vergessen: Lifecycle Rules. Mit diesen Regeln lässt sich für Dateien festlegen, was mit ihnen während oder nach ihrem Lebenszyklus passieren soll. Ein Use Case ist: Automatisches Löschen. Genau, was ich brauchte! Und das beste daran: Diese Löschung findet Backend-intern statt und muss nicht durch Tools von außen gesteuert werden. Mein Timeout-Problem bei der Dateiauflistung konnte ich so geschickt umgehen.&lt;/p&gt;
&lt;p&gt;Ich habe also das &lt;code&gt;mc&lt;/code&gt; Tool genutzt, um alle Dateien in &lt;code&gt;/cache&lt;/code&gt; nach 15 Tagen ablaufen zu lassen:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mc ilm add minio/metalheadclub-media/cache --expire-days &lt;span style="color:#ae81ff"&gt;15&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fertig!&lt;/p&gt;
&lt;p&gt;Der aktuelle Status lässt sich mit&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mc admin scanner status minio/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;einsehen. Damit der Minio-interne Dateiscanner schneller arbeitet und Dateien somit schneller löscht, wenn sie ihr Haltbarkeitsdatum überschritten haben, kann der Vorgang noch etwas beschleunigt werden:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mc admin config set minio/ scanner speed&lt;span style="color:#f92672"&gt;=&lt;/span&gt;fastest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="abschluss"&gt;Abschluss&lt;/h2&gt;
&lt;p&gt;Nach 15 Tagen waren alle alten Dateien gelöscht und ich konnte das Minio-Bucket löschen, Minio entfernen und das alte Bucket auch aus meinen Nginx-Konfigurationen entfernen. Die Zeilen&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Migration Workaround ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;proxy_intercept_errors&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;recursive_error_pages&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;error_page&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;404&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;403&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;@minio&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;und&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;@minio&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;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;wurden entfernt und Nginx neu geladen. Metalhead.club war nun vollständig zu SeaweedFS migriert.&lt;/p&gt;
&lt;p&gt;Ein Dank geht übrigens an Stefano Marinelli (&lt;a href="https://mastodon.bsd.cafe/@stefano"&gt;@stefano@bsd.cafe&lt;/a&gt;) für den &lt;a href="https://it-notes.dragas.net/2025/11/06/self-hosting-your-mastodon-media-with-seaweedfs/"&gt;SeaweedFS Artikel unter FreeBSD&lt;/a&gt;. Dieser hat mich letztendlich dazu bewegt, SeaweedFS eine Chance zu geben und die Umstellung anzupacken.&lt;/p&gt;</description></item><item><title>Alte Dovecot Konfiguration zu Dovecot 2.4 (Debian Trixie) migrieren</title><link>https://thomas-leister.de/mailserver-migrate-config-to-dovecot-2.4-debian-trixie/</link><pubDate>Sat, 01 Nov 2025 10:10:33 +0100</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/mailserver-migrate-config-to-dovecot-2.4-debian-trixie/</guid><description>&lt;p&gt;Seit einigen Wochen ist die neue Debian-Version &amp;ldquo;Trixie&amp;rdquo; zum Upgrade von Debian &amp;ldquo;Bookworm&amp;rdquo; erhältlich - und damit haben sich auch einige meiner Leser zu einer Aktualisierung ihres Mailserver-Setups aus meiner &lt;a href="https://thomas-leister.de/mailserver-debian-buster/"&gt;&amp;ldquo;Mailserver mit Dovecot, Postfix, MySQL und Rspamd unter Debian 10 Buster [v1.0]&amp;rdquo;&lt;/a&gt; Mailserveranleitung entschieden. Das für Debian Buster beschriebene Setup funktioniere nämlich genauso gut auch für Debian Bookworm - aber mit Debian Trixie kommt der Bruch:&lt;/p&gt;
&lt;div class="warning"&gt;
Die neueste Debian Version macht bei der mitgelieferte Dovecot-Version einen Sprung auf Dovecot 2.4. Damit sind alte Dovecot-Konfigurationen nicht mehr kompatibel!
&lt;/div&gt;
&lt;p&gt;Die Konfigurations-Syntax hat sich an einigen Punkten stark geändert und alte Konfigurationen aus vorherigen Dovecot-Versionen können nicht mehr eingelesen werden. Dieser Beitrag geht auf die Änderungen in der neuen Version 2.4 ein und erklärt die Migration anhand des Beispiels der oben erwähnten Mailserveranleitung. Alle Änderungen spielen sich in der Datei &lt;code&gt;/etc/dovecot/dovecot.conf&lt;/code&gt; ab. Weitere Änderungen (z.B. an Datenbankschema o.Ä.) sind nicht notwendig.&lt;/p&gt;
&lt;div class="tip"&gt;
Eine vollständige Konfigurationsdatei findet ihr am Ende dieses Beitrags.
&lt;/div&gt;
&lt;p&gt;Insgesamt hat sich verändert:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Die Art und Weise, wie Konfigurationsparameter geschachtelt werden können&lt;/li&gt;
&lt;li&gt;Namen einzelner Parameter&lt;/li&gt;
&lt;li&gt;Variablennamen und -Funktionen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alle Änderungen sind in der &lt;a href="https://doc.dovecot.org/main/installation/upgrade/2.3-to-2.4.html"&gt;Dovecot-Dokumentation&lt;/a&gt; beschrieben. Wer beim Setup seines Mailservers meiner Anleitung gefolgt ist, kann jedoch einfach weiterlesen und die beschriebenen Schritte ausführen, um zu einer funktionierenden Konfiguration zu kommen.&lt;/p&gt;
&lt;h2 id="schritt-1-config-versionsnummern-hinzufügen"&gt;Schritt 1: Config-Versionsnummern hinzufügen&lt;/h2&gt;
&lt;p&gt;Dovecot 2.4 führt eine Versionierung der Konfigurationssyntax ein. Deshalb müssen &lt;strong&gt;am Anfang der Datei&lt;/strong&gt; folgende Zeilen unbedingt eingefügt werden:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Dovecot config and storage versions
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;Das Versäumnis aus der Vergangenheit scheint man nun nachgeholt zu haben ;-). Zukünftige Änderungen an der Syntax können dann von Dovecot selbst abgefangen werden bzw. bei Änderungen gewarnt werden.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="ssl-einstellungen"&gt;SSL-Einstellungen&lt;/h2&gt;
&lt;p&gt;Die SSL-Einstellungen werden wie folgt umbenannt bzw. verändert:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ssl_cert&lt;/code&gt; =&amp;gt; &lt;code&gt;ssl_server_cert_file&lt;/code&gt; (und spitze Klammer zu Beginn weglassen)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_key&lt;/code&gt; =&amp;gt; &lt;code&gt;ssl_server_key_file&lt;/code&gt; (und spitze Klammer zu Beginn weglassen)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_dh&lt;/code&gt; =&amp;gt; &lt;code&gt;ssl_server_dh_file&lt;/code&gt; (und spitze Klammer zu Beginn weglassen)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_prefer_server_ciphers&lt;/code&gt; =&amp;gt; &lt;code&gt;ssl_server_prefer_ciphers&lt;/code&gt;. Werte nicht &amp;ldquo;yes&amp;rdquo; oder &amp;ldquo;no&amp;rdquo;, sondern &amp;ldquo;client&amp;rdquo; oder &amp;ldquo;server&amp;rdquo;. Default: Client. Einstellung kann entfernt werden.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_min_protocol&lt;/code&gt;: kann weggelassen werden, default ist bereits TLSv1.2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disable_plaintext_auth=yes&lt;/code&gt; =&amp;gt; &lt;code&gt;auth_allow_cleartext=no&lt;/code&gt;. Ist default - kann weggelassen werden.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Insgesamt ergibt sich also:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssl = required
ssl_server_cert_file = /etc/acme.sh/mydomain.tld/fullchain.pem
ssl_server_key_file = /etc/acme.sh/mydomain.tld/privkey.pem
ssl_server_dh_file = /etc/dovecot/dh4096.pem
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="passdb--userdb-und-auth_username_format"&gt;PassDB / UserDB und auth_username_format&lt;/h2&gt;
&lt;p&gt;Die beiden PassDB und UserDB Abschnitte werden zu:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;passdb sql {
query = SELECT username AS user, domain, password FROM accounts WHERE username = &amp;#39;%{user | username | lower }&amp;#39; AND domain = &amp;#39;%{user | domain | lower}&amp;#39; and enabled = true;
default_password_scheme = SHA512-CRYPT
}
userdb sql {
query = SELECT concat(quota, &amp;#39;M&amp;#39;) AS quota_storage_size FROM accounts WHERE username = &amp;#39;%{user | username | lower }&amp;#39; AND domain = &amp;#39;%{user | domain | lower}&amp;#39; AND sendonly = false;
iterate_query = SELECT username, domain FROM accounts where sendonly = false;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Achtet speziell auf die angepassten Variablen, z.B. &lt;code&gt;%{user | username | lower }&lt;/code&gt;. Hinter dem &lt;code&gt;concat(quota, 'M') AS quota_storage_size&lt;/code&gt; versteckt sich außerdem ein kleiner Bugfix, den ich hier direkt einbringen will. ;-)&lt;/p&gt;
&lt;p&gt;Außerdem wird die Variable auch bei &lt;code&gt;auth_username_format&lt;/code&gt; angepasst:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;auth_mechanisms = plain login
auth_username_format = %{user | lower }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Damit die gerade definierten UserDB und PassDB Abschnitte überhaupt funktionieren, werden die Zugangsdaten zur MySQL-Datenbank in einem neuen SQL-Abschnitt definiert:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sql_driver = mysql
mysql /var/run/mysqld/mysqld.sock {
user = vmail
password = mypassword
dbname = vmail
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;&lt;code&gt;mypassword&lt;/code&gt; muss selbstverständlich angepasst werden!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Die Datei &lt;code&gt;/etc/dovecot/dovecot-sql.conf&lt;/code&gt; kann vollständig gelöscht werden - sie wird nicht mehr benötigt.&lt;/p&gt;
&lt;h2 id="mail-location"&gt;Mail location&lt;/h2&gt;
&lt;p&gt;Die Definition des Mailspeicherorts &lt;code&gt;mail_location&lt;/code&gt; wird in mehrere Parameter aufgeteilt und hierdurch ersetzt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mail_driver = maildir
mail_path = ~/mail
mailbox_list_layout = fs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Außerdem bekommt der &lt;code&gt;mail_home&lt;/code&gt; Parameter neue Variablennamen:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mail_home = /var/vmail/mailboxes/%{user | domain }/%{user | username }
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="protocols"&gt;Protocols&lt;/h2&gt;
&lt;p&gt;Die &lt;code&gt;protocols&lt;/code&gt; Einstellung und die beiden &lt;code&gt;protocol imap&lt;/code&gt; und &lt;code&gt;protocol lmtp&lt;/code&gt; Sections werden zu diesem Block:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;protocols {
lmtp = yes
imap = yes
sieve = yes
}
protocol imap {
mail_plugins {
imap_quota = yes
imap_sieve = yes
}
mail_max_userip_connections = 50
imap_idle_notify_interval = 29 mins
}
protocol lmtp {
mail_plugins {
sieve = yes
notify = yes
push_notification = yes
}
postmaster_address = postmaster@mydomain.tld
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="plugins-section"&gt;Plugins Section&lt;/h2&gt;
&lt;p&gt;Das &lt;code&gt;plugins&lt;/code&gt; Segment&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;plugins {
...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;gibt es nicht mehr. Stattdessen wird der Inhalt direkt global.&lt;/p&gt;
&lt;h3 id="sieve-plugineinstellungen"&gt;Sieve Plugineinstellungen&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sieve_plugins&lt;/code&gt;, &lt;code&gt;sieve_before&lt;/code&gt; und &lt;code&gt;sieve&lt;/code&gt; werden zu:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sieve_plugins {
sieve_imapsieve = yes
sieve_extprograms = yes
}
sieve_script spam-global {
type = before
path = /var/vmail/sieve/global/spam-global.sieve
}
sieve_script personal {
type = personal
path = /var/vmail/sieve/%{user | domain }/%{user | username }/scripts
active_path = /var/vmail/sieve/%{user | domain }/%{user | username }/active-script.sieve
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Alle &lt;code&gt;imapsieve*&lt;/code&gt; Parameter werden insgesamt zu:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# From Mailbox to Spam
mailbox Spam {
sieve_script spam {
type = before
cause = copy
path = /var/vmail/sieve/global/learn-spam.sieve
}
}
# From Spam to another folder (learn HAM)
imapsieve_from Spam {
sieve_script ham {
type = before
cause = copy
path = /var/vmail/sieve/global/learn-ham.sieve
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;sieve_global_extensions&lt;/code&gt; wird zu:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sieve_global_extensions {
vnd.dovecot.pipe = yes
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="quota"&gt;Quota&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;quota&lt;/code&gt; und &lt;code&gt;quota_exceeded_message&lt;/code&gt; werden zu:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;quota &amp;#34;User quota&amp;#34; {
driver = count
}
quota_exceeded_message = Benutzer %{user} hat das Speichervolumen ueberschritten. / User %{user} has exhausted allowed storage space.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Der alte &amp;ldquo;fs&amp;rdquo; Treiber für Quota soll nicht mehr genutzt werden. Stattdessen wird nun &lt;code&gt;count&lt;/code&gt; genutzt. &lt;code&gt;quota_exceeded_message&lt;/code&gt; bekommt außerdem neue Variablennamen.&lt;/p&gt;
&lt;p&gt;Im neuen &lt;code&gt;mail_plugins&lt;/code&gt; Blog wird das Quota-Plugin aktiviert:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mail_plugins {
quota = yes
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Im vorher bereits erwähnten &lt;code&gt;protocol imap&lt;/code&gt; Block wurde außerdem noch &lt;code&gt;imap_quota&lt;/code&gt; aktiviert.&lt;/p&gt;
&lt;h2 id="änderung-der-zugriffsrechte"&gt;Änderung der Zugriffsrechte&lt;/h2&gt;
&lt;p&gt;Da nun die MySQL-Zugangsdaten direkt in der Hauptkonfigurationsdatei liegen und nicht mehr in eine extra Datei ausgelagert sind, werden die Zugriffsrechte auf &lt;code&gt;dovecot.conf&lt;/code&gt; weiter eingeschränkt. Außer root soll niemand mehr die Datei lesen können:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod &lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; /etc/dovecot/dovecot.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="vollständige-beispielkonfiguration"&gt;Vollständige Beispielkonfiguration&lt;/h2&gt;
&lt;p&gt;Für den einfacheren Vergleich hier nochmal die vollständige Datei &lt;code&gt;/etc/dovecot/dovecot.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Dovecot config and storage versions
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
###
### Protocol settings
#############################
protocols {
lmtp = yes
imap = yes
sieve = yes
}
protocol imap {
mail_plugins {
imap_quota = yes
imap_sieve = yes
}
mail_max_userip_connections = 50
imap_idle_notify_interval = 29 mins
}
protocol lmtp {
mail_plugins {
sieve = yes
notify = yes
push_notification = yes
}
postmaster_address = postmaster@mydomain.tld
}
##
## TLS Config
## Quelle: https://ssl-config.mozilla.org/#server=dovecot&amp;amp;version=2.3.9&amp;amp;config=intermediate&amp;amp;openssl=1.1.1d&amp;amp;guideline=5.4
##
ssl = required
ssl_server_cert_file = /etc/acme.sh/mydomain.tld/fullchain.pem
ssl_server_key_file = /etc/acme.sh/mydomain.tld/privkey.pem
ssl_server_dh_file = /etc/dovecot/dh4096.pem
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
###
### Dovecot services
################################
service imap-login {
inet_listener imap {
port = 143
}
}
service managesieve-login {
inet_listener sieve {
port = 4190
}
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0660
group = postfix
user = postfix
}
}
service auth {
### Auth socket für Postfix
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
### Auth socket für LMTP-Dienst
unix_listener auth-userdb {
mode = 0660
user = vmail
group = vmail
}
}
###
### SQL settings
###
sql_driver = mysql
mysql /var/run/mysqld/mysqld.sock {
user = vmail
password = mypassword
dbname = vmail
}
###
### Client authentication
#############################
auth_mechanisms = plain login
auth_username_format = %{user | lower }
passdb sql {
query = SELECT username AS user, domain, password FROM accounts WHERE username = &amp;#39;%{user | username | lower }&amp;#39; AND domain = &amp;#39;%{user | domain | lower}&amp;#39; and enabled = true;
}
userdb sql {
query = SELECT concat(quota, &amp;#39;M&amp;#39;) AS quota_storage_size FROM accounts WHERE username = &amp;#39;%{user | username | lower }&amp;#39; AND domain = &amp;#39;%{user | domain | lower}&amp;#39; AND sendonly = false;
iterate_query = SELECT username, domain FROM accounts where sendonly = false;
}
##
### Address tagging
###
recipient_delimiter = +
###
### Mail location
#######################
mail_uid = vmail
mail_gid = vmail
mail_privileged_group = vmail
mail_home = /var/vmail/mailboxes/%{user | domain }/%{user | username }
mail_driver = maildir
mail_path = ~/mail
mailbox_list_layout = fs
###
### Mailbox configuration
########################################
namespace inbox {
inbox = yes
mailbox Spam {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Sent {
auto = subscribe
special_use = \Sent
}
}
###
### Mail plugins
############################
mail_plugins {
quota = yes
}
sieve_plugins {
sieve_imapsieve = yes
sieve_extprograms = yes
}
sieve_script spam-global {
type = before
path = /var/vmail/sieve/global/spam-global.sieve
}
sieve_script personal {
type = personal
path = /var/vmail/sieve/%{user | domain }/%{user | username }/scripts
active_path = /var/vmail/sieve/%{user | domain }/%{user | username }/active-script.sieve
}
# From Mailbox to Spam
mailbox Spam {
sieve_script spam {
type = before
cause = copy
path = /var/vmail/sieve/global/learn-spam.sieve
}
}
# From Spam to another folder (learn HAM)
imapsieve_from Spam {
sieve_script ham {
type = before
cause = copy
path = /var/vmail/sieve/global/learn-ham.sieve
}
}
# Sieve extensions only allowed in global context
sieve_global_extensions {
vnd.dovecot.pipe = yes
}
sieve_pipe_bin_dir = /usr/bin
###
### IMAP Quota
###########################
quota_exceeded_message = Benutzer %{user} hat das Speichervolumen ueberschritten. / User %{user} has exhausted allowed storage space.
quota &amp;#34;User quota&amp;#34; {
driver = count
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="updates"&gt;Updates&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2026-02-06: &lt;code&gt;default_password_scheme&lt;/code&gt; in &lt;code&gt;passdb&lt;/code&gt; Abschnitt hinzugefügt. Abschnitt &amp;ldquo;Änderung der Zugriffsrechte&amp;rdquo; hinzugefügt.&lt;/li&gt;
&lt;li&gt;2025-11-26: &lt;code&gt;user = vmail&lt;/code&gt; aus &lt;code&gt;service lmtp&lt;/code&gt; entfernt, um &lt;code&gt;lmtp(1301): Error: conn unix:/run/dovecot/anvil: net_connect_unix(/run/dovecot/anvil) failed: Permission denied&lt;/code&gt; zu vermeiden.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Spamhaus blockiert Mailserver trotz eigenen Resolvers</title><link>https://thomas-leister.de/spamhaus-blocks-mailserver-open-resolver/</link><pubDate>Sat, 01 Nov 2025 10:06:44 +0100</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/spamhaus-blocks-mailserver-open-resolver/</guid><description>&lt;p&gt;Kürzlich hatte ich einen kuriosen Zwischenfall bei einem meiner Kunden: Der Mailserver, der nach meiner Mailserver-Anleitung aufgesetzt wurde, lehnte seit einiger Zeit immer wieder einmal E-Mails von außen ab. Zunächst konnte ich den Fehler nicht nachstellen, doch nach einigen Testmails gelang es mir schließlich, dem Problem auf die Spur zu kommen &amp;hellip;&lt;/p&gt;
&lt;p&gt;Als Antwort auf meine Testmail bekam ich schließlich eine E-Mail von meinem Mailer Daemon zurück mit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="mailto:user@domain.tld"&gt;user@domain.tld&lt;/a&gt;: host mail.domain.tld[xx.xx.xx.xx] refused to
talk to me: 521 5.7.1 Service unavailable; client [xx.xx.xx.xx] blocked
using zen.spamhaus.org&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Merkwürdig. War mein eigener Mailserver auf den Spamhaus Zen Blockliste gelandet? Ein schneller Check auf &lt;a href="https://check.spamhaus.org"&gt;https://check.spamhaus.org&lt;/a&gt; zeige zum Glück das Gegenteil. Doch warum wurde die E-Mail dann abgelehnt? Ein Blick in das &lt;code&gt;mail.log&lt;/code&gt; offenbarte mehr Details:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Oct 25 12:50:54 sisyphos postfix/dnsblog[12662]: addr xxxx:xxxx:xxxx::xxxx listed by domain zen.spamhaus.org as 127.255.255.254
[...]
Oct 25 12:50:54 sisyphos postfix/dnsblog[12662]: addr xx.xx.xx.xx listed by domain zen.spamhaus.org as 127.255.255.254
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hier kann man erkennen, wie das Mailsystem die beiden Zustellversuche meines Mailservers (einmal IPv4, einmal IPv6) bewertet: Spamhaus liefert für beide Versuche eine Antwort &lt;code&gt;127.255.255.254&lt;/code&gt; zurück - laut der &lt;a href="https://www.spamhaus.org/faqs/dnsbl-usage/#200"&gt;Tabelle unter &amp;ldquo;What do the 127.&lt;em&gt;.&lt;/em&gt;.* return codes mean in DNSBL?&amp;rdquo;&lt;/a&gt; steht das für einen Fehler:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;127.255.255.254 Query via public/open resolver&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Der Fehler ist mir nicht ganz unbekannt, weil viele meiner Leser ihre Mailserver vor vielen Jahren aufgesetzt haben, als offene Resolver für Spamhaus noch kein Problem waren. Doch seit einiger Zeit sperrt Spamhaus Mailserver aus, die über einen offenen DNS-Resolver auf die DNS-Blocklisten (und damit die Nameserver von Spamhaus) zugreifen. Die Lösung dafür ist ganz einfach: Mit einem lokalen, eigenen Resolver, z.B. via &lt;code&gt;unbound&lt;/code&gt;, umgeht man das Problem.&lt;/p&gt;
&lt;p&gt;Mein Kunde setzte auf fraglichem Mailserver aber bereits einen lokalen Resolver ein. Das konnte also nicht das Problem sein.&lt;/p&gt;
&lt;p&gt;Dennoch: Tests via &lt;code&gt;dig&lt;/code&gt; zeigten immer wieder (aber nicht &lt;em&gt;immer&lt;/em&gt;!) an, dass ein Code &lt;code&gt;127.255.255.254&lt;/code&gt; zurückgegeben wurde:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@mail /etc/postfix # dig @127.0.0.1 135.72.1.5.zen.spamhaus.org
; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.11.3-1ubuntu1.18-Ubuntu &amp;lt;&amp;lt;&amp;gt;&amp;gt; @127.0.0.1 135.72.1.5.zen.spamhaus.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: NOERROR, id: 26448
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;135.72.1.5.zen.spamhaus.org. IN A
;; ANSWER SECTION:
135.72.1.5.zen.spamhaus.org. 1588 IN A 127.255.255.254
;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Oct 25 13:30:31 CEST 2025
;; MSG SIZE rcvd: 74
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Eigentlich sollte in diesem Fall ein &lt;code&gt;NXDOMAIN&lt;/code&gt; zurückgegeben werden.&lt;/p&gt;
&lt;h2 id="schritt-1-symptome-lindern"&gt;Schritt 1: Symptome lindern&lt;/h2&gt;
&lt;p&gt;Die Ursache für das Verhalten von Spamhaus war noch unklar. Also bemühte ich mich, die negativen Auswirkungen auf meinen Kunden erst einmal einzudämmen - schließlich war er durch den Fehler nur sporadisch erreichbar. Die zurückgegebene IP-Adresse &lt;code&gt;127.255.255.254&lt;/code&gt; wurde durch Postfix nämlich als &amp;ldquo;blockieren - Spam!&amp;rdquo; interpretiert. In der alten Anleitung, auf der der Mailserver basierte, hatte ich nämlich nicht genau spezifiziert, welche Return Codes als Spam gewertet werden sollten. Das habe ich nachgeholt. Die Konfiguration sieht nun so aus:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;### DNS blocklists
postscreen_dnsbl_threshold = 2
postscreen_dnsbl_sites = zen.spamhaus.org=127.0.0.[2..11]*2
postscreen_dnsbl_action = drop
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nun werden nur IP-Adressen endend auf 2-11 als Spamalarm gewertet. Andere Adressen lassen Postscreen kalt. Damit führt auch der &amp;ldquo;Open Resolver&amp;rdquo; Code nicht mehr zur Ablehnung eingehender E-Mails. Allerdings funktioniert der Zugriff auf die Spamhaus Zen Blockliste noch immer nicht zuverlässig. Die Maßnahme verhindert nur die durch den Fehler verursachten False Positives.&lt;/p&gt;
&lt;h2 id="schritt-2-hilfe-bei-spamhaus-anfordern"&gt;Schritt 2: Hilfe bei Spamhaus anfordern&lt;/h2&gt;
&lt;p&gt;Eine Anfrage bei Spamhaus zu dem Problem half nur bedingt. Auf die Frage, ob Spamhaus eine Liste mit offenen Resolvern pflege und darin vielleicht fälschlicherweise der Kundenserver stehe, hieß es:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The IPs are not listed in our mirrors but I would recommend using the IPv4 for queries. We are seeing abuse of our mirrors from IPv6 space of Hetzner in millions so some IPv6 queries might fail.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Das hat mich dann eine Idee gebracht &amp;hellip;&lt;/p&gt;
&lt;h2 id="schritt-3-spamhaus-nur-via-ipv4-ansprechen"&gt;Schritt 3: Spamhaus nur via IPv4 ansprechen&lt;/h2&gt;
&lt;p&gt;Schnell kam der Verdacht auf, dass das zufällig erscheinende Verhalten evtl. mit einem Unterschied zwischen IPv4 und IPv6-basierten Anfragen zu tun haben könnte. Das bestätigte sich sofort, als ich den lokalen Unbound-Resolver auf &amp;ldquo;IPv4 only&amp;rdquo; Betrieb umgestellt hatte. Folgende Konfiguration in &lt;code&gt;/etc/unbound/unbound.conf.d/ipv4-only.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server:
do-ip4: yes
do-ip6: no
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;hellip; führt dazu, dass Unbound bei der Kontaktaufnahme zu anderen Nameservern (z.B. den Servern von Spamhaus) kein IPv6 mehr verwendet. DNS-Antworten können trotzdem noch v6-Adressen enthalten - diese bleiben unberührt. Funktional bleibt der Mailserver also unverändert - aber die Spamhaus DNSBL funktionierte mit diesem Kniff wieder wunderbar.&lt;/p&gt;
&lt;h2 id="war-das-schon-alles"&gt;War das schon alles?&lt;/h2&gt;
&lt;p&gt;Bei meiner Recherche bin ich außerdem auf einen zweiten Punkt gestoßen, der für das Problem gesorgt haben könnte. Denn DNS-Resolver, die mit Spamhaus genutzt werden, müssen laut &lt;a href="https://www.spamhaus.org/resource-hub/email-security/query-the-legacy-dnsbls-via-hetzner/"&gt;diesem Artikel&lt;/a&gt; einen Reverse DNS Eintrag vorweisen können. Fehlt dieser, kann dies ebenfalls Probleme bei der Nutzung der Spamhaus Zen Blockliste verursachen. Es ist also darauf zu achten, dass sowohl die IPv4 Adresse als auch die IPv6 Adresse einen gültigen Reverse DNS Eintrag haben.&lt;/p&gt;
&lt;h2 id="alternative-lösung-mit-dem-spamhaus-dqs"&gt;Alternative Lösung mit dem Spamhaus DQS&lt;/h2&gt;
&lt;p&gt;Statt des DNSBL hätte ich meinen Kunden auch auf den &lt;a href="https://docs.spamhaus.com/datasets/docs/source/40-real-world-usage/MTAs/020-Postfix.html"&gt;Spamhaus DQS Service&lt;/a&gt; umstellen können. Mit dem Service habe ich allerdings noch keine Erfahrung gesammelt. Zudem bedingt die Nutzung eine Registrierung. Die weitere Nutzung der öffentlichen DNSBL lag daher auf der Hand.&lt;/p&gt;</description></item><item><title>Incus Container zwischen Hosts kopieren oder verschieben</title><link>https://thomas-leister.de/move-incus-container-between-hosts-zfs/</link><pubDate>Sun, 19 Oct 2025 14:27:54 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/move-incus-container-between-hosts-zfs/</guid><description>&lt;p&gt;Im folgenden erkläre ich kurz die Übertragung von Incus-Container zwischen zwei Incus-Hosts, die nicht in einem gemeinsamen Cluster hängen. Beide Hosts sind in meinem Fall mit ZFS-basiertem Containerstorage ausgerüstet. Siehe auch: &lt;a href="https://thomas-leister.de/incus-in-vm-with-zfs-pool/"&gt;Incus mit ZFS in einer VM betreiben und Container auf neuen Storagepool verschieben&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="api-öffnen"&gt;API öffnen&lt;/h2&gt;
&lt;p&gt;Wenn ein Container zwischen zwei Hosts verschoben werden soll, muss zuerst ein Übertragungsweg zwischen den beiden Incus-Hosts geschaffen werden. Dazu wird auf dem Quellhost die API am Netzwerkinterface verfügbar gemacht:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus config set core.https_address [2001:db8::1]:8443
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Die IPv6-Adresse in eckigen Klammern muss selbstverständlich durch die IP-Adresse eures eigenen Interfaces ersetzt werden. Bei einer IPv4-Adresse wird auf die Klammern verzichtet.&lt;/p&gt;
&lt;p&gt;Als nächstes wird ein Trust Token auf dem Quellserver erzeugt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus config trust add sourcehost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Auf dem Zielserver wird der Quellserver hinzugefügt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus remote add sourcehost 2001:db8::1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dabei muss am Ende der Trust Token eingegeben werden, der zuvor vom Quellserver ausgegeben wurde. Sobald in der Konsole&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Client certificate now trusted by server: sourcehost
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;erscheint, kann eine erste Übertragung des Containers vom Quellserver zum Zielserver stattfinden.&lt;/p&gt;
&lt;h2 id="daten-übertragen"&gt;Daten übertragen&lt;/h2&gt;
&lt;p&gt;Das Root Filesystem des Containers kann auf zwei Arten und Weisen übertragen werden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a) An einem Stück mit einer Downtime, die so lange dauert, wie die Datenübertragung. Empfohlen für kleine Container.&lt;/li&gt;
&lt;li&gt;b) In mehreren Schritten über einen zweiten, inkrementellen Sync. Mit wenigen Sekunden bis Minuten Downtime. Empfohlen vor allem für größere Container mit 100+ GB. Funktioniert nur mit Dateisystemen wie ZFS, die Snapshots unterstützen.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-einfache-übertragung-für-kleinere-container"&gt;a) Einfache Übertragung für kleinere Container&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# (auf dem Quellserver)
incus stop mycontainer
# (auf dem Zielserver)
incus copy sourcehost:mycontainer mycontainer --stateless
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="oder-b-inkrementelle-übertragung"&gt;Oder b): Inkrementelle Übertragung&lt;/h3&gt;
&lt;p&gt;Bei der inkrementellen Übertragung wird zuerst im Hintergrund der aktuelle Stand des Container übertragen. Die Anteile, die während der Übertragung verändert wurden (weil der Container noch in Betrieb war), werden in einem zweiten Schritt nachgeliefert. Dieser zweite Sync läuft dann aber wesentlich schneller, weil er nur ein Delta Überträgt und nicht &lt;em&gt;alle&lt;/em&gt; Daten nochmal.&lt;/p&gt;
&lt;p&gt;Snapshot auf dem Quellserver erstellen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus snapshot create mycontainer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Der Container bleibt in Betrieb und wird noch nicht gestoppt)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Initiale Übertragung auf dem Zielserver starten:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus copy sourcehost:mycontainer mycontainer --stateless
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nach der ersten Übertragung wird der Quellcontainer beendet und ein zweiter, finaler Sync angestoßen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# (auf dem Quellserver)
incus stop mycontainer
incus snapshot create mycontainer
# (auf dem Zielserver)
incus copy sourcehost:mycontainer mycontainer --refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Schließlich wird der Container auf dem Zielserver gestartet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus config unset mycontainer volatile.apply_template
incus start mycontainer
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="api-schließen"&gt;API schließen&lt;/h2&gt;
&lt;p&gt;Zum Schluss kann die API auf dem Quellserver wieder dicht gemacht werden, falls sie nicht für andere Zwecke verwendet werden soll:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus config unset core.https_address
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Incus mit ZFS in einer VM betreiben und Container auf neuen Storagepool verschieben</title><link>https://thomas-leister.de/incus-in-vm-with-zfs-pool/</link><pubDate>Sun, 19 Oct 2025 12:12:10 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/incus-in-vm-with-zfs-pool/</guid><description>&lt;p&gt;Seit vielen Jahren setze ich beim Hosting meiner Services auf das Virtualisierungs- und Containermanagement-Tool &amp;ldquo;Incus&amp;rdquo; (vorher &amp;ldquo;LXD&amp;rdquo;). Incus läuft dabei in einer virtuellen Maschine und hilft mir, eine Separierung auf Applikationsebene herzustellen. So gibt es beispielsweise einen Incus-Container für trashserver.net, einen weiteren für metalhead.club usw. Die Root-Dateisysteme der einzelnen Container befinden sich dabei in einem ZFS-Dateisystem. So kann ich platzsparende Snapshots von meinen Containern vor kritischen Wartungsaktionen anlegen, z.B. vor Updates oder Betriebssystemupgrades.&lt;/p&gt;
&lt;p&gt;Da ich den zugrundeliegenden Speicher kürzlich erneuert habe, will ich hier mein Storage-Setup kurz vorstellen und für mich (aber auch euch ;-) ) dokumentieren, auf was ich geachtet habe und wie ich meine Container auf den neuen Storage umgezogen habe.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/incus-in-vm-with-zfs-pool/images/storage-setup.drawio.svg" alt="Storage setup diagram"&gt;&lt;/p&gt;
&lt;p&gt;Auf Hypervisor-Level (also auf dem physischen Server) wird der Speicher für meine Container in Form von LVM Volumes bereitgestellt. Das erlaubt es mir, den großen Speicher in vielen einzelnen Volumes auch für andere VMs anzubieten und je nach Ressourcenbedarf zu dimensionieren.&lt;/p&gt;
&lt;p&gt;Ein LVM Volume (Blockdevice) wird dann über libvirt/QEMU und den virtio-blk Treiber an die VM durchgereicht. Dabei nutze ich in libvirt die folgenden Parameter für den Speicher:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Treiber: virt-blk&lt;/li&gt;
&lt;li&gt;Cache Modus: none (cache macht schon zfs)&lt;/li&gt;
&lt;li&gt;io = native (meistens besser als io=threads)&lt;/li&gt;
&lt;li&gt;discard = unmap (für Trim-Support)&lt;/li&gt;
&lt;li&gt;serial = megastor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Besonders hervorheben will ich an dieser Stelle die &lt;code&gt;serial&lt;/code&gt; Einstellung, die es erlaubt, für das durchgereichte Blockdevice eine Seriennummer / eine ID / einen eindeutigen Namen anzugeben. Bindet man mehrere Speichergeräte in seine VM an, lassen sich die einzelnen Speicher so wieder zweifelsfrei identifizieren. Ein &lt;code&gt;megastor&lt;/code&gt; benanntes Gerät erscheint in der VM dann z.B. als &lt;code&gt;/dev/disk/by-id/virtio-megastor&lt;/code&gt;. Das ist vor allem hilfreich, wenn &amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;hellip; man nicht mehr sicher ist, was eigentlich zwischen /dev/vdb und /dev/vdd steckt und welcher Storage gleich nochmal welcher ist?&lt;/li&gt;
&lt;li&gt;&amp;hellip; oder man /dev/vdc entfernen will, aber man befürchten muss, dass dann beim nächsten Reboot /dev/vdd nachrückt und diesen Platz einnimmt. Dann wäre das Chaos perfekt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Denn: &lt;strong&gt;Device-Namen für Storage-Devices sind unter Linux nicht &amp;ldquo;stabil&amp;rdquo;!&lt;/strong&gt; Nur Partitions-UUIDs sind es (oder alternativ Disk-IDs!).&lt;/p&gt;
&lt;p&gt;In der VM wiederum wird das Blockdevice genutzt, um damit einen einfachen ZFS-Pool zu erzeugen, z.B. via:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zpool create -o ashift=12 megastor /dev/disk/by-id/virtio-megastor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Der ZFS Pool wird auch in Incus registriert, damit er mit den Containern genutzt werden kann:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus storage create megastor zfs source=megastor --description=&amp;quot;megastor storage for containers&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="trim-support"&gt;Trim Support&lt;/h2&gt;
&lt;p&gt;Vor einiger Zeit unterstützte nur der virtio-scsi Treiber &lt;code&gt;trim&lt;/code&gt;, um freie Speicherbereiche auch auf der SSD passend zu markieren und freizugeben. Regelmäßiges &amp;ldquo;trimmen&amp;rdquo; führt zu einer wesentlich besseren Performance. Seit einigen QEMU Versionen beherrscht aber auch der schlankere &amp;ldquo;virtio-blk&amp;rdquo; Treiber Trimming.&lt;/p&gt;
&lt;p&gt;ZFS führt über das Paket &lt;code&gt;zfsutils-linux&lt;/code&gt; übrigens regelmäßig selbst ein Trim-Cronjob (&lt;code&gt;/etc/cron.d/zfsutils-linux&lt;/code&gt;) aus - es ist also nicht nötig, selbst ein &lt;code&gt;fstrim&lt;/code&gt; auszuführen.&lt;/p&gt;
&lt;p&gt;Allerdings ist darauf zu achten, dass - wie oben erwähnt - &lt;code&gt;discard=unmap&lt;/code&gt; für das QEMU Speicherberät definiert ist. Und auch auf Hypervisor-Level muss in meinem Fall wegen der LUKS-Verschlüsselung in &lt;code&gt;/etc/crypttab&lt;/code&gt; ein &amp;ldquo;discard&amp;rdquo; Parameter eingetragen sein. Sonst wird das Trimming nicht bis in die unseren Storage-Layer durchgereicht. Beispiel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;luks-d429b69b-1c1a-234a-8bcf-64232e83f156 UUID=d429b69b-1c1a-234a-8bcf-64232e83f156 /var/lib/megastor.key discard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Außerdem sollte auch LVM passend konfiguriert sein: &lt;code&gt;vim /etc/lvm/lvm.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;issue_discards = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="bestehende-incus-vms-auf-neuen-storage-verschieben"&gt;Bestehende Incus VMs auf neuen Storage verschieben&lt;/h2&gt;
&lt;p&gt;Da ich - wie zu Beginn erwähnt - neue SSDs in dem System verbaut und dementsprechend neue ZFS Pools eingerichtet habe, sollten nun auch die Incus Container in den neuen &amp;ldquo;megastor&amp;rdquo; Pool verschoben werden. Dafür gibt es mehrere Methoden &amp;hellip;&lt;/p&gt;
&lt;h3 id="der-einfache-weg-mit-einigen-minuten-downtime"&gt;Der einfache Weg mit einigen Minuten Downtime&lt;/h3&gt;
&lt;p&gt;Das geht mit Incus so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus stop mycontainer
incus move mycontainer --storage megastor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vor der Übertragung wird der Container gestoppt, was je nach Größe des Containers und Performance des Storage eine gewisse Weile dauert und somit Downtime verursacht. Dafür ist die Methode weniger komplex. Sie lohnt sich bei eher kleinen Containern mit etwa 50 - 100 GB Größe.&lt;/p&gt;
&lt;h3 id="mit-inkrementeller-übertragung---aber-kurzer-downtime"&gt;Mit inkrementeller Übertragung - aber kurzer Downtime&lt;/h3&gt;
&lt;p&gt;Wer Downtime möglichst vermeiden will, kann anders vorgehen:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Snapshot vom Quellcontainer während des laufenden Betriebs erzeugen&lt;/li&gt;
&lt;li&gt;Snapshot im Hintergrund zum Ziel kopieren&lt;/li&gt;
&lt;li&gt;Quellcontainer stoppen (Beginn Downtime)&lt;/li&gt;
&lt;li&gt;Differenz seit letzter Übertragung übertragen (idR. nur wenige MG bis GB)&lt;/li&gt;
&lt;li&gt;Alten Container entfernen, neuen Container umbenennen&lt;/li&gt;
&lt;li&gt;Neuen Container starten (Ende Downtime)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Die Downtime reduziert sich auf ein Minimum. Das grundsätzliche Vorgehen habe ich schon einmal in meinem Beitrag &amp;ldquo;&lt;a href="https://thomas-leister.de/en/move-a-lxd-based-mastodon-instance/"&gt;Move a Mastodon instance with less than 3 minutes downtime (LXD/ZFS-based)&lt;/a&gt;&amp;rdquo; beschrieben und habe gewissermaßen um LXD herum gearbeitet. Dasselbe funktioniert aber auch mit Incus-Bordmitteln und kann auf die Übertragung eines Containers von einem Storage auf den anderen angewendet werden (nicht nur auf die Übertragung zwischen zwei Hosts!).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# 1. Snapshot im laufenden Betrieb erstellen
incus snapshot create mycontainer
# 2. Snapshot auf neuen Storage &amp;#34;megastor&amp;#34; übertragen
incus copy mycontainer new-mycontainer --storage megastor
# 3. Container stoppen und weiteren Snapshot erzeugen
incus stop mycontainer
incus snapshot create mycontainer
# 4. Änderungen seit Beginn der ersten Übertragung nun auch übertragen
incus copy mycontainer new-mycontainer --storage megastor --refresh
# 5. Alten Container löschen, neuen Container umbenennen
incus delete mycontainer
incus move new-mycontainer mycontainer
# 6. Neuen Container starten
incus config unset mycontainer volatile.apply_template
incus start mycontainer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Das &lt;code&gt;incus config unset...&lt;/code&gt; sorgt dafür, dass der neue Container die alten MAC-Adressen, IPs etc behält und keine neuen Adressen zugeteilt bekommt.&lt;/p&gt;
&lt;p&gt;Wer will, kann übrigens zwischen Schritt 2 und 3 auch weitere &amp;ldquo;snapshot - copy&amp;rdquo; Folgen zwischenschieben:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus snapshot create mycontainer
incus copy mycontainer new-mycontainer --storage megastor --refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Das ist vor allem dann sinnvoll, wenn nach der ersten Übertragung viel Zeit vergangen ist. Hat sich während dieser Zeit viel im Container geändert, fällt auch der finale Sync bedeutend länger aus und sorgt für eine längere Downtime. Dem kann man entgegenwirken, indem man sich in mehreren &amp;ldquo;snapshot - copy&amp;rdquo; Schritten annähert. Wichtig ist nur, dass nach dem ersten &lt;code&gt;copy&lt;/code&gt; Befehl das &lt;code&gt;--refresh&lt;/code&gt; Flag nicht vergessen wird.&lt;/p&gt;</description></item><item><title>Hetzner S3 Bucket mit Restic und Rclone sichern</title><link>https://thomas-leister.de/hetzner-s3-bucket-mit-restic-sichern/</link><pubDate>Sun, 28 Sep 2025 15:39:33 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/hetzner-s3-bucket-mit-restic-sichern/</guid><description>&lt;p&gt;Kürzlich habe ich für einen Kunden den S3-Storage für eine Mastodon Instanz von Minio S3 auf ein extern gehostetes Hetzner S3 umgestellt. Das bringt auch eine Änderung bei der Datensicherung mit sich: Statt einfach nur das lokale Minio-Dateisystem zu sichern, muss sich für das Hetzner S3 Bucket eine andere Lösung überlegt werden. Im Folgenden will ich meine Lösung dafür aufzeigen.&lt;/p&gt;
&lt;h2 id="ein-s3-bucket-in-die-restic-sicherung-einbinden-nicht-so-einfach"&gt;Ein S3 Bucket in die Restic-Sicherung einbinden? Nicht so einfach!&lt;/h2&gt;
&lt;p&gt;Die Sicherung des Minio-Dateisystems war einfach und bequem. Restic musste an dieser Stelle nicht anders eingestellt werden, als für die übliche Datensicherung auf dem Linux-Server. Da Minio nicht mit einer Datenbank arbeitet, sondern einzig und allein mit dem Dateisystem, muss bei der Datensicherung über Restic nichts besonderes beachtet werden.&lt;/p&gt;
&lt;p&gt;Da die Medien für die fragliche Mastodon Instanz nun aber nicht mehr lokal gehostet, sondern über ein Hetzner S3 Bucket bereitgestellt werden, stellt sich die Frage, wie mit dem Restic-basierten Backupmechanismus damit umgegangen werden soll. Eine Lösung könnte sein, das Produktiv-Bucket einfach in einem readonly-Modus zu einem weiteren Bucket zu kopieren und diese Buckets beispielsweise täglich zu synchronisieren. Allerdings hätte man damit keine inkrementellen, täglichen Sicherungen und kann höchstens auf den Stand des vorherigen Tages zurückspringen. Zudem arbeitet diese Lösung an Restic vorbei - ich hätte gerne alle Daten an einem Ort.&lt;/p&gt;
&lt;h2 id="das-s3-bucket-mit-rclone-als-virtuelles-dateisystem-einbinden"&gt;Das S3 Bucket mit rclone als virtuelles Dateisystem einbinden&lt;/h2&gt;
&lt;p&gt;Eine zweite Lösung kann sein, das Hetzner S3 Bucket mit einem Tool wie z.B. rclone lokal als virtuelles Filesystem zu mounten und dieses dann von dort zu sichern. Das mag umständlich und nicht sehr schnell sein, sorgt aber dafür, dass ich sämtliche Mediendateien in den gewohnten Restic Vorhaltezeiten gesichert habe und entsprechend auch wieder einfach herstellen kann, wenn es nötig sein sollte.&lt;/p&gt;
&lt;p&gt;Mein Backup-Script sieht nun in etwa so aus:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Restic Passwort und Speicherort einlesen&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;source /opt/backup/resticenv.sh
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Mount S3 bucket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone mount &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --daemon &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --read-only &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vfs-cache-mode full &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vfs-cache-max-size 20G &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --vfs-cache-max-age 24h &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --dir-cache-time 72h &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --poll-interval 15s &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --checkers &lt;span style="color:#ae81ff"&gt;128&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --transfers &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --multi-thread-streams &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --timeout 1m &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --retries &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --use-mmap &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --s3-chunk-size 64M &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hetzner-s3:mastodon-media /mnt/hetzner-s3/mastodon-media
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;### Restic ausführen&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;restic backup &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --exclude-file /opt/backup/exclude.list &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /home &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /etc &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /root &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /mnt/hetzner-s3/mastodon-media/accounts &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /mnt/hetzner-s3/mastodon-media/custom_emojis &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /mnt/hetzner-s3/mastodon-media/media_attachments &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /mnt/hetzner-s3/mastodon-media/site_uploads
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /mnt/hetzner-s3/mastodon-media/cache/accounts \&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /mnt/hetzner-s3/mastodon-media/cache/custom_emojis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;restic forget --prune &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --keep-last &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --keep-daily &lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --keep-weekly &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --keep-monthly &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;restic check
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Unmount S3 bucket&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fusermount -u /mnt/hetzner-s3/mastodon-media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das &lt;code&gt;rclone&lt;/code&gt; Kommando zu Beginn des Scripts mountet das S3 bucket als virtuelles Dateisystem unter &lt;code&gt;/mnt/hetzner-s3/mastodon-media&lt;/code&gt;. Das &lt;code&gt;restic backup&lt;/code&gt; Kommando kann sich in diesem Verzeichnis bedienen und die Inhalte sichern. Weitere Einstellungen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--daemon&lt;/code&gt; sorgt dafür, dass rclone im Hintergrund läuft. Andernfalls würde das Script blockieren.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--read-only&lt;/code&gt; stellt sicher, dass das Mountpoint readonly ist, um das eingehängt S3 Bucket nicht zu verändern.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--vfs-cache-max-size 20G&lt;/code&gt; erlaubt rclone, bis zu 20 GB als Cache zu nutzen. Ich hoffe auf eine schnellere Ausführung.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--checkers 128&lt;/code&gt; erlaubt rclone bis zu 128 &amp;ldquo;checker&amp;rdquo; Instanzen. Das sind die Teile von rclone, die das Bucket nach Dateien durchsuchen. Die Anpassung hat in meinem Fall dafür gesorgt, dass die Sicherung durch restic deutlich schneller funktioniert hat als vorher. Allerdings sollte geprüft werden, ob so nicht die &lt;a href="https://docs.hetzner.com/storage/object-storage/overview#limits"&gt;maximale Anzahl von Anfragen / Verbindungen&lt;/a&gt; an das Bucket überschritten werden kann, wenn nebenher auch noch andere Prozesse auf die Daten zugreifen.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Die beiden letzten Pfade unter &lt;code&gt;cache/&lt;/code&gt; habe ich vorerst ausgeklammert. Dort speichert Mastodon gecachte Versionen von Medien fremder Instanzen. Diese sollten nach dem Einspielen eines Backups eigentlich von Mastodon selbst wieder bei den fremden Instanzen angefragt werden, wenn sie fehlen sollten. Ich sichere sie deshalb aktuell nicht mit. Allerdings bleibt das noch ein Punkt, der noch weiter untersucht werden muss - denn in der Vergangenheit hat das erneute Herunterladen schon einmal Schwierigkeiten gemacht und nicht wie gedacht funktioniert. Insbesondere fehlende Avatare unter &lt;code&gt;cache/accounts/&lt;/code&gt; wären ärgerlich. Auf der anderen Seite ist die Datenmenge besonders groß und es tummeln sich u.U. hunderttausende kleinste Dateien in diesem Verzeichnis. Das ist für den Backupprozess besonders belastend und führt dazu, dass Backups sehr sehr lange dauern können.&lt;/p&gt;
&lt;p&gt;Mit &lt;code&gt;fusermount&lt;/code&gt; wird das S3 Bucket nach dem Backup schließlich wieder ausgehängt.&lt;/p&gt;
&lt;h2 id="vorsicht-mit-alten-softwareversionen"&gt;Vorsicht mit alten Softwareversionen!&lt;/h2&gt;
&lt;p&gt;Ein Hinweis noch zu verwendeten Softwareversionen: Ich hatte anfangs während des Restic-Backups Fehlermeldungen wie&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;error: NodeFromFileInfo: xattr.list /mnt/hetzner-s3/7de24bcbedb9b634.jpg: input/output error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Die Lösung für meine Probleme bestand darin, sowohl Restic als auch Rclone auf aktuellere Versionen zu aktualisieren, welche von meiner Debian-Distribution noch nicht angeboten wurden. Siehe auch Thread im Restic Forum: &lt;a href="https://forum.restic.net/t/creating-a-restic-backup-of-a-s3-bucket/10335/2"&gt;&amp;ldquo;Creating a Restic backup &lt;em&gt;of&lt;/em&gt; a S3 bucket&amp;rdquo;&lt;/a&gt;. Für restic Version 0.18.1 und Version 1.71.1 kann ich aber bestätigen, dass die Sicherungen auf die oben beschriebene Art und Weise funktionieren.&lt;/p&gt;
&lt;div class="warning"&gt;
Seht meine Konfiguration als experimentell an - ich bin selbst noch nicht sicher, wo / ob sich hier u.U. noch etwas optimieren lässt. Das Backup funktioniert in einer akzeptablen Zeit - aber das Hereinnehmen von &lt;code&gt;cache/&lt;/code&gt; bringt den Mechanismus wegen der extrem vielen Dateien ans Limit. Es kann dann durchaus länger als 24 Stunden dauern, bis alle Dateien gesichert sind.
&lt;/div&gt;</description></item><item><title>Mastodon S3 Storage von self-hosted Minio zu Hetzner S3 übertragen</title><link>https://thomas-leister.de/mastodon-s3-storage-von-minio-zu-hetzner-s3-uebertragen/</link><pubDate>Wed, 24 Sep 2025 17:32:04 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/mastodon-s3-storage-von-minio-zu-hetzner-s3-uebertragen/</guid><description>&lt;p&gt;Im Auftrag eines Kunden hatte ich kürzlich die Aufgabe, ein S3 Bucket für eine Mastodon-Instanz von einer selbst gehosteten Minio-Instanz zu Hetzner S3 zu übertragen. Das Vorgehen und meine genutzten Konfigurationen will ich hier erklären.&lt;/p&gt;
&lt;p&gt;Die Mastodon-Instanz des Kunden wurde bisher durch eine Minio-Instanz auf demselben Server mit Medien und Mediencache versorgt. Allerdings wuchs der Mediencache mit zunehmender Vernetzung der Mastodon-Instanz immer weiter an. Selbst durch eine Reduzierung der Cache-Haltedauer von wenigen Tagen waren die Kosten für den S3-Speicher zu hoch. Denn dieser befand sich auf einem leistungsstarken, aber verhältnismäßig teuren externen Volume eines Hetzner Cloudservers.&lt;/p&gt;
&lt;p&gt;Ende 2024 führte Hetzner den Hetzner S3 Speicher in sein Produktportfolio ein - somit eröffnete sich eine attraktive Alternative zur Minio-Instanz. Eine Alternative, die vor allem durch geringe Kosten glänzen kann. Die Kosten für ein 300 GB Volume beliefen sich auf knapp 16 € / Monat. Nicht sehr viel für ein Business - aber sehr wohl für eine kleine, spendenfinanzierte Mastodon-Instanz. Der Preis für 1000 GB (!) Hetzner S3 Speicher liegt im Vergleich nur bei knapp 6 € / Monat. Die Umstellung war also schnell beschlossen.&lt;/p&gt;
&lt;h2 id="schritt-1-parallelbetrieb-zweier-buckets"&gt;Schritt 1: Parallelbetrieb zweier Buckets&lt;/h2&gt;
&lt;p&gt;Um den Umstieg auf den neuen Speicher für die Nutzer so angenehm wie möglich zu gestalten und die Downtime gering zu halten, entschloss ich mich dazu, beide Buckets für die Zeit der Datenmigration parallel laufen zu lassen.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Altes Bucket &amp;ldquo;minio&amp;rdquo;: Liefert die bisher gespeicherten Mediendateien aus (readonly)&lt;/li&gt;
&lt;li&gt;Neues Bucket &amp;ldquo;hetzner-s3&amp;rdquo;: Nimmt neue Mediendateien entgegen, speichert diese und liefert diese aus&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Der Vorgeschaltete Nginx-Proxy wurde so konfiguriert, dass bei einer Dateianfrage zuerst das neue Bucket befragt wurde. Konnte die angefragte Datei dort nicht gefunden werden, wurde das alte S3 Bucket befragt.&lt;/p&gt;
&lt;p&gt;Proxykonfiguration vorher:&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;upstream&lt;/span&gt; &lt;span style="color:#e6db74"&gt;minio&lt;/span&gt; {
&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; 127.0.0.1:&lt;span style="color:#ae81ff"&gt;9000&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&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; {
&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;mastodon-media.s3.domain.tld&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Authorization&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Methods&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Headers&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-id-2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-request-id&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-bucket-region&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amzn-requestid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_ignore_headers&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Real-IP&lt;/span&gt; $remote_addr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-For&lt;/span&gt; $proxy_add_x_forwarded_for;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-Proto&lt;/span&gt; $scheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_http_version&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;chunked_transfer_encoding&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Cache-Status&lt;/span&gt; $upstream_cache_status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span style="color:#e6db74"&gt;nosniff&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Content-Security-Policy&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;default-src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;; &lt;span style="color:#f92672"&gt;form-action&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;https://minio&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Proxykonfiguration nachher - mit Parallelbetreib beider Buckets (Fallback auf altes Bucket):&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;upstream&lt;/span&gt; &lt;span style="color:#e6db74"&gt;hetzner&lt;/span&gt; {
&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; &lt;span style="color:#e6db74"&gt;mastodon-media.nbg1.your-objectstorage.com&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;upstream&lt;/span&gt; &lt;span style="color:#e6db74"&gt;minio&lt;/span&gt; {
&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; 127.0.0.1:&lt;span style="color:#ae81ff"&gt;9000&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&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; {
&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;resolver&lt;/span&gt; 9.9.9.9;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;mastodon-media.nbg1.your-objectstorage.com&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Authorization&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Methods&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Headers&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-id-2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-request-id&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-bucket-region&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amzn-requestid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_ignore_headers&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Real-IP&lt;/span&gt; $remote_addr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-For&lt;/span&gt; $proxy_add_x_forwarded_for;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-Proto&lt;/span&gt; $scheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_http_version&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;chunked_transfer_encoding&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Cache-Status&lt;/span&gt; $upstream_cache_status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span style="color:#e6db74"&gt;nosniff&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Content-Security-Policy&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;default-src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;; &lt;span style="color:#f92672"&gt;form-action&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;https://hetzner&lt;/span&gt;; &lt;span style="color:#75715e"&gt;# This uses the upstream directive definition to load balance
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Fallback to oldbucket
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_intercept_errors&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;error_page&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;404&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;403&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;@oldbucket&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;location&lt;/span&gt; &lt;span style="color:#e6db74"&gt;@oldbucket&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Host&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;mastodon-media.s3.domain.tld&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Authorization&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Methods&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Headers&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-id-2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-request-id&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-meta-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-server-side-encryption&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amz-bucket-region&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_hide_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;x-amzn-requestid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_ignore_headers&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Set-Cookie&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Real-IP&lt;/span&gt; $remote_addr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-For&lt;/span&gt; $proxy_add_x_forwarded_for;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Forwarded-Proto&lt;/span&gt; $scheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_http_version&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_set_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Connection&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;chunked_transfer_encoding&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;off&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Cache-Control&lt;/span&gt; &lt;span style="color:#e6db74"&gt;public&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Access-Control-Allow-Origin&amp;#39;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Cache-Status&lt;/span&gt; $upstream_cache_status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span style="color:#e6db74"&gt;nosniff&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;add_header&lt;/span&gt; &lt;span style="color:#e6db74"&gt;Content-Security-Policy&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;default-src&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&lt;/span&gt;; &lt;span style="color:#f92672"&gt;form-action&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;none&amp;#39;&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;proxy_pass&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http://minio&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Maßgeblich sind hier der neue &amp;ldquo;location&amp;rdquo; Block &amp;ldquo;@oldbucket&amp;rdquo; und der verweis darauf, falls Dateien um neuen Bucket nicht gefunden werden können:&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Fallback to oldbucket
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;proxy_intercept_errors&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;on&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;error_page&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;404&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;403&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;@oldbucket&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wichtig ist an der Stelle, nicht nur 404-Fehler abzufangen, sondern auch 403-Fehler (&amp;ldquo;forbidden&amp;rdquo;). Der Hetzner S3 Storage reagiert offenbar mit diesem Fehlercode, wenn eine Ressource angefragt wird, deren Elternverzeichnisse (noch) nicht existieren.&lt;/p&gt;
&lt;h2 id="schritt-2-speichern-neuer-medien-in-dem-neuen-bucket"&gt;Schritt 2: Speichern neuer Medien in dem neuen Bucket&lt;/h2&gt;
&lt;p&gt;Bis hierher dürften die Nutzer der Mastodon-Instanz kaum etwas mitbekommen haben. Die Proxy-Änderung ist in Sekundenbruchteilen aktiv und ist für den User völlig transparent. Im Folgenden kommt es allerdings zu einer kurzen, mehrsekündigen Downtime, denn die Konfiguration von Mastodon wird angepasst und daraufhin müssen alle Mastodon Services neu gestartet werden.&lt;/p&gt;
&lt;p&gt;Für das neue Bucket wurden beispielhaft folgende Einstellungen in der &lt;code&gt;.env.production&lt;/code&gt; von Mastodon getroffen:&lt;/p&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_ENABLED&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_BUCKET&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;mastodon-media&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt;myaccessid&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;lt;myaccesskey&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_ALIAS_HOST&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;media.domain.tld&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_REGION&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;nbg1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_PROTOCOL&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;https&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_HOSTNAME&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;media.domain.tld&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_ENDPOINT&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;https://nbg1.your-objectstorage.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;S3_SIGNATURE_VERSION&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Durch einen Neustart der Mastodon-Dienste werden die Änderungen aktiv. Neu eintreffende Mediendateien werden nun nun an ausschließlich in dem neuen S3-Bucket abgelegt:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cd /etc/systemd/system
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl restart mastodon-*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach wenigen Sekunden steht Mastodon wieder bereit.&lt;/p&gt;
&lt;h2 id="schritt-3-alte-daten-zu-neuem-bucket-migrieren"&gt;Schritt 3: Alte Daten zu neuem Bucket migrieren&lt;/h2&gt;
&lt;p&gt;Die Kuh ist vom Eis - der kritische Teil ist erledigt. Nun können ganz entspannt die alten Daten ebenfalls in das neue Bucket transportiert werden. Dazu nutze ich das Tool &amp;ldquo;rclone&amp;rdquo;, mit dem sich zwei S3-Buckets sehr einfach miteinander synchronisieren lassen. Dazu werden für die beiden S3-Provider zunächst Konfigurationen unter &lt;code&gt;~/.config/rclone/rclone.conf&lt;/code&gt; anlegegt. Hier wieder exemplarisch:&lt;/p&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[hetzner-s3]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;s3&lt;/span&gt;
&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;Other&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;access_key_id&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;accessid&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;secret_access_key&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;accesskey&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;endpoint&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;nbg1.your-objectstorage.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;acl&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;public-read&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;region&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;nbg1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[minio-s3]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;s3&lt;/span&gt;
&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;Minio&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;access_key_id&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;accessid&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;secret_access_key&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;lt;accesskey&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;endpoint&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;s3.domain.tld&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;acl&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;public-read&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;region&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;server1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Endpoint, Region und Access-ID sowie Access-Key müssen natürlich auf das jeweilige Bucket angepasst werden. Wichtig ist noch die Einstellung &lt;code&gt;acl = public-read&lt;/code&gt;, die dafür sorgt, dass übertragene Dateien die korrekten Berechtigungen tragen. Wird die Einstellung nicht korrekt gesetzt, sind die Dateien u.U. nicht öffentlich lesbar und der Medienspeicher kann nicht im Sinne von Mastodon verwendet werden.&lt;/p&gt;
&lt;p&gt;Nun folgt eine Reihe von rclone-Kommandos, mit denen die Mediendateien vom alten &lt;code&gt;minio-s3&lt;/code&gt; zum neuen &lt;code&gt;hetzner-s3&lt;/code&gt; Speicher umgezogen werden. Zu beachten ist, dass hier das &lt;code&gt;copy&lt;/code&gt; Subcommand von &lt;code&gt;rclone&lt;/code&gt; verwendet wird. Dieses synchronisiert keine Löschungen, sondern transportiert nur nicht-existierende Dateien von A nach B. Anders als &lt;code&gt;sync&lt;/code&gt;! Dieses würde inzwischen neu eingetroffene Dateien im neuen Bucket löschen. Es ist also Vorsicht geboten. &lt;code&gt;copy&lt;/code&gt; ist das richtige Kommando für unseren Zweck.&lt;/p&gt;
&lt;p&gt;Die wichtigsten Dateien zuerst:&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/custom_emojis/ hetzner-s3:mastodon-media/custom_emojis/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/accounts/ hetzner-s3:mastodon-media/accounts/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/site_uploads/ hetzner-s3:mastodon-media/site_uploads/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/media_attachments/ hetzner-s3:mastodon-media/media_attachments/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/imports/ hetzner-s3:mastodon-media/imports/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/cache/accounts/ hetzner-s3:mastodon-media/cache/accounts/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/cache/custom_emojis/ hetzner-s3:mastodon-media/cache/custom_emojis/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Danach folgen die weniger wichtigen Mediendateien. Dabei handelt es sich nur um zwischengespeicherte Medien und Vorschaubilder für Webseiten. Wer Zeit und Traffic sparen will, kann diese auch weglassen (oder die Vorhaltezeit für diese Medien vorab in Mastodon auf wenige Tage reduzieren - dann fällt die zu übertragende Datenmenge deutlich kleiner aus!).&lt;/p&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/cache/preview_cards/ hetzner-s3:mastodon-media/cache/preview_cards/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rclone copy --progress --transfers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt; minio-s3:mastodon-media/cache/media_attachments/ hetzner-s3:mastodon-media/cache/media_attachments/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieser Vorgang kann durchaus einen halben Tag dauern - je nach Leistung der beteiligten Server und der Größe des Buckets.&lt;/p&gt;
&lt;h2 id="schritt-4-altes-bucket-abklemmen"&gt;Schritt 4: Altes Bucket abklemmen&lt;/h2&gt;
&lt;p&gt;Sobald alle Daten übertragen sind, empfiehlt es sich, das alte Bucket noch eine Weile zu behalten, falls Daten nicht korrekt übertragen wurden. Man kann das alte Bucket allerdings schon einmal testhalber vom Proxy abklemmen und prüfen, ob denn die Mastodon-Instanz korrekt ohne das alte Bucket läuft. Dazu wird der &lt;code&gt;@oldbucket&lt;/code&gt; location-Block entfernt sowie die folgenden Zeilen auskommentiert (oder entfernt):&lt;/p&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-nginx" data-lang="nginx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#upstream minio {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server 127.0.0.1:9000;
&lt;/span&gt;&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Fallback to oldbucket
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#proxy_intercept_errors on;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#error_page 404 403 = @oldbucket;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach einem &lt;code&gt;systemctl reload nginx&lt;/code&gt; wird nur noch das neue Bucket genutzt und die Funktion der Website lässt sich überprüfen. Nicht vergessen: Für ernstzunehmende Testa auch einmal den Browsercache leeren!&lt;/p&gt;
&lt;p&gt;Läuft auch nach ein paar Tagen noch alles prima, kann das alte Bucket gelöscht werden. Fertig!&lt;/p&gt;</description></item><item><title>Automatischer Dark Mode für Drawio Diagramme mittels CSS Filter</title><link>https://thomas-leister.de/dark-mode-for-drawio-diagrams/</link><pubDate>Fri, 01 Aug 2025 06:05:59 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/dark-mode-for-drawio-diagrams/</guid><description>&lt;p&gt;In meinem neuesten Blogpost &amp;ldquo;&lt;a href="https://thomas-leister.de/nginx-http3-quic-proxy-wrong-virtual-host/"&gt;Nginx HTTP/3 Proxyserver zeigt Inhalte vom falschen Virtual Host&lt;/a&gt;&amp;rdquo; habe ich ein Diagramm von Diagrams.net / Drawio eingebunden, das eine SVG-Datei ist.&lt;/p&gt;
&lt;p&gt;Wenn man sein Diagramm aus Draw.io exportiert, kann man wählen, ob man für die exportierte Grafik ein helles oder ein dunkles Farbthema anwenden will. Ich wähle hier normalerweise das helle, damit sich das Diagramm gut in das helle Default-Theme von thomas-leister.de einfügt. Aber wie gehe ich damit in der dunklen Variante meines Blogs um? Benutzer sehen automatisch das dunkle Theme, wenn sie ihr Betriebssystem entsprechend eingestellt haben. Ich habe mich gefragt, ob es nicht einen einfachen Weg gibt, um die hellen SVG Grafiken bei Bedarf in dunkle Varianten umzuwandeln. Schließlich kennt CSS ja mittlerweile &lt;code&gt;filter&lt;/code&gt;, oder?&lt;/p&gt;
&lt;p&gt;Zum Glück habe ich ziemlich schnell eine einfache Lösung für das Problem finden können: &amp;ldquo;&lt;a href="https://dev.to/akhilarjun/one-line-dark-mode-using-css-24li"&gt;One line - Dark Mode using CSS&lt;/a&gt;&amp;rdquo;. Und das ist der Trick, den ich gefunden habe:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;filter: invert(1) hue-rotate(180deg);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Diese CSS-Zeile invertiert zunächst alle Farben (also von hell nach dunkel und umgekehrt). Dabei wird schwarz in weiß umgewandelt (und umgekehrt), aber Farben werden auch umgekehrt. Das ist nicht beabsichtigt. Deshalb werden über ein &lt;code&gt;hue-rotate&lt;/code&gt; Farbige Inhalte wieder zurückkonvertiert.&lt;/p&gt;
&lt;p&gt;In meinem CSS Stylesheet meines Blogs habe ich folgendes implementiert:&lt;/p&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:#66d9ef"&gt;media&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;prefers-color-scheme&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;dark&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt; * Invert colors for SVG diagrams
&lt;/span&gt;&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;figure&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;diagram&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;filter&lt;/span&gt;: invert(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) hue-rotate(&lt;span style="color:#ae81ff"&gt;180&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;deg&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Von der Umwandlung sind nur Grafiken betroffen, die sich in einem &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; HTML element der Klasse &lt;code&gt;diagram&lt;/code&gt; befinden - und nur dann, wenn der Benutzer ein dunkles Theme in seinen Betriebssystemeinstellungen gewählt hat.&lt;/p&gt;
&lt;p&gt;Wenn ich nun in einem Blogeintrag eine SVG-Zeichnung einbinden will, mache ich das über den folgenden Shortcode:&lt;/p&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-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{&amp;lt; &lt;span style="color:#f92672"&gt;figure&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;images/my-drawing.drawio.svg&amp;#34;&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:#e6db74"&gt;&amp;#34;My description&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;diagram&amp;#34;&lt;/span&gt; &amp;gt;}}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und kann das Ergebnis aussehen (oben im Dark Mode - unten im Light Mode):&lt;/p&gt;
&lt;p class="full-width"&gt;&lt;img src="https://thomas-leister.de/dark-mode-for-drawio-diagrams/images/example-diagram-comparison.png" alt="Example screenshot of a diagram on my blog"&gt;&lt;/p&gt;</description></item><item><title>Nginx HTTP/3 Proxyserver zeigt Inhalte vom falschen Virtual Host</title><link>https://thomas-leister.de/nginx-http3-quic-proxy-wrong-virtual-host/</link><pubDate>Wed, 30 Jul 2025 17:38:27 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/nginx-http3-quic-proxy-wrong-virtual-host/</guid><description>&lt;p&gt;Ende Mai 2025 habe ich den &lt;a href="https://metalhead.club"&gt;metalhead.club&lt;/a&gt; Song über eine &lt;a href="https://thomas-leister.de/bandcamp-alternative-faircamp/"&gt;Faircamp-Website&lt;/a&gt; unter &lt;a href="https://music.metalhead.club"&gt;music.metalhead.club&lt;/a&gt; veröffentlicht.
Doch nicht alle Nutzer konnten meinen Links auf die Faircamp-Seite problemlos folgen. In wenigen Einzelfällen berichteten mir User von mindestens einem der folgenden Fehler:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wenn Nutzer die &lt;a href="https://music.metalhead.club/metalheadclub-anthem/"&gt;Albumseite&lt;/a&gt; aufriefen: Fehler 404 - not found&lt;/li&gt;
&lt;li&gt;Wenn Nutzer die &lt;a href="https://music.metalhead.club"&gt;Hauptseite&lt;/a&gt; aufriefen: Der &lt;a href="https://watch.metalhead.club"&gt;watch.metalhead.club Globus&lt;/a&gt; wurde angezeigt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nach einigen Versuchen konnte ich den Fehler sporadisch selbst reproduzieren. In den Access-Logs des Nginx-Proxys ist mir dabei aufgefallen, dass alle fehlerhaften Anfragen mit HTTP/3 gestellt wurden. Das war mein erster Anknüpfungspunkt.&lt;/p&gt;
&lt;p&gt;Da der &lt;a href="https://blog.cloudflare.com/cloudflare-view-HTTP/3-usage/"&gt;Anteil der HTTP/3-kompatiblen Webbrowser stetig steigt&lt;/a&gt;, hatte ich mich einige Monate zuvor nämlich entschlossen, auf dem Server eine sehr aktuelle Nginx-Version mit aktiviertem HTTP/3 zu nutzen. Doch: Für music.metalhead.club hatte ich HTTP/3 &lt;em&gt;nicht&lt;/em&gt; aktiviert. Da stellt sich zuerst die Frage: &amp;hellip;&lt;/p&gt;
&lt;h2 id="wieso-wird-eigentlich-http3-genutzt"&gt;Wieso wird eigentlich HTTP/3 genutzt?&lt;/h2&gt;
&lt;p&gt;Für meine Mastodon Instanz unter metalhead.club war als einzigen virtual Host auf dem Nginx-Server HTTP/3 aktiviert, um vor allem für weiter entfernte Benutzer den Verbindungsaufbau zu verringern und so Ladezeiten zu verkürzen. Für diesen virtual Host war auch der &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Alt-Svc"&gt;Alt-Svc Header passend auf &amp;ldquo;h3&amp;rdquo;&lt;/a&gt; (also HTTP/3) gesetzt. Doch diese Einstellung galt &lt;em&gt;nicht&lt;/em&gt; für andere virtual Hosts wie z.B. music.metalhead.club - wieso also stellten verschiedene Webbrowser &lt;em&gt;dennoch&lt;/em&gt; HTTP/3-basierte Anfragen an den nicht-HTTP/3 virtual Host music.metalhead.club ?&lt;/p&gt;
&lt;p&gt;Um das Phänomen zu erklären, muss man wissen, dass HTTP/3 ebenso wie sein Vorgänger HTTP/2 eine Funktion namens &amp;ldquo;Connection Coalescing&amp;rdquo; kennt. Wenn &amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP-Adresse&lt;/li&gt;
&lt;li&gt;Port und&lt;/li&gt;
&lt;li&gt;SSL-Zertifikat&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;gleich sind, werden &amp;ldquo;alte&amp;rdquo; QUIC-Verbindungen wiederverwendet (siehe auch &lt;a href="https://datatracker.ietf.org/doc/html/rfc7540#section-9.1.1"&gt;RFC 7540 &amp;ldquo;Connection reuse&amp;rdquo;&lt;/a&gt; - auch &amp;ldquo;&lt;a href="https://blog.cloudflare.com/connection-coalescing-experiments/"&gt;Connection Coalescing&lt;/a&gt; genannt&amp;rdquo;). Obwohl also womöglich später ein anderer virtual Host auf dem Server angesprochen wird, wird eine &lt;strong&gt;bestehende QUIC-Verbindung zum Server verwendet, falls die drei Parameter übereinstimmen&lt;/strong&gt;. Ob dieser &amp;ldquo;andere&amp;rdquo; virtual Host überhaupt HTTP/3 unterstützt, wird dabei nicht mehr überprüft. In meinem Fall ist offenbar folgendes passiert:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ein Nutzer ruft zuerst metalhead.club auf. Der Browser erkennt den HTTP/3-Support über den &amp;ldquo;Alt-Svc&amp;rdquo; Header&lt;/li&gt;
&lt;li&gt;Weitere Verbindungen zu metalhead.club werden nun mit HTTP/3 gestellt - dazu wird eine QUIC Verbindung aufgebaut&lt;/li&gt;
&lt;li&gt;Der Benutzer surft nun weiter zu music.metalhead.club. Dieser andere virtual Host verwendet dieselbe IP / Port / SSL-Zertifikat-Kombination (weil Wildcard-Zertifikat). Der Browser erkennt dies und benutzt die bereits bestehende QUIC-Verbindung&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So kommt es, dass der Webbrowser mit dem Webserver nun auch HTTP/3 spricht, wenn es um music.metalhead.club geht.&lt;/p&gt;
&lt;h2 id="-doch-musicmetalheadclub-spricht-kein-quic"&gt;&amp;hellip; doch music.metalhead.club spricht kein QUIC!&lt;/h2&gt;
&lt;p&gt;In der Nginx-Konfiguration für music.metalhead.club gibt es allerdings keinen QUIC Listener. Und hier kommt die zweite Stolperfalle ins Spiel: Für den Fall, dass über eine QUIC-Verbindung ein virtual Host angefordert wird, der gar keinen QUIC-Listener hat, sucht sich Nginx den &amp;ldquo;default&amp;rdquo; QUIC virtual Host heraus. Der &amp;ldquo;default&amp;rdquo; vHost ist jener, der explizit als solcher festgelegt wurde - bzw. jener, der (wie in meinem Fall) durch die Konfigurationsreihenfolge implizit von Nginx festgelegt wurde. In meinem Fall gab es nur &lt;em&gt;einen&lt;/em&gt; vHost, der QUIC spricht: den vHost für metalhead.club.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anfragen an music.metalhead.club wurden also letztendlich vom vHost für metalhead.club verarbeitet, weil es sonst keinen vHost mit eingeschaltetem QUIC gab.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Da dieser vHost nur ein Proxy ist, der auf eine weitere Nginx-Instanz (&amp;ldquo;Nginx app server&amp;rdquo;) zeigt, wird die Anfrage nochmal weitergeleitet. Die App-Server Nginx Instanz in meinem Setup bekommt nun schließlich die Anfrage bezüglich music.metalhead.club - kann damit aber nichts anfangen. Schließlich besitzt sie selbst keinen vHost, der für music.metalhead.club zuständig sein könnte (siehe Diagramm). Also wird wieder einmal ein &amp;ldquo;default&amp;rdquo; vHost ausgesucht. Und da watch.metalhead.club der erste vHost ist, der in der Konfiguration des zweiten Nginx vorkommt, wird die Anfrage an watch.metalhead.club weitergereicht und dort beantwortet: Der Nutzer sieht den Globus auf der Startseite&amp;hellip; oder eine 404-Fehlerseite, weil die angeforderte Albumübersicht auf watch.metalhead.club natürlich nicht existiert.&lt;/p&gt;
&lt;figure class="diagram"&gt;&lt;img src="https://thomas-leister.de/nginx-http3-quic-proxy-wrong-virtual-host/images/nginx-http3-problem.drawio.svg"
alt="Rot: Die fehlerhafte Route zwischen den beiden Nginx-Instanzen. Grün: Die Soll-Route. (1) Wegen des fehlenden QUIC-Listeners wird der metalhead.club vHost angesprochen. (2) Dieser wiederum zeigt nun auf den falschen Nginx App-Server."&gt;&lt;figcaption&gt;
&lt;p&gt;Rot: Die fehlerhafte Route zwischen den beiden Nginx-Instanzen. Grün: Die Soll-Route. (1) Wegen des fehlenden QUIC-Listeners wird der metalhead.club vHost angesprochen. (2) Dieser wiederum zeigt nun auf den falschen Nginx App-Server.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="die-lösung"&gt;Die Lösung&lt;/h2&gt;
&lt;p&gt;Rätsel gelöst! Doch wie lässt sich das Problem beheben?
Das Kernproblem liegt darin, dass Webbrowser annehmen, dass QUIC Verbindungen auch für andere virtual Hosts wiederverwendet werden können, wenn IP-Adresse, Port und Zertifikat dieselben sind. Deshalb sollten wir die passenden Bedingungen dafür auf der Serverseite schaffen. &lt;strong&gt;Die Problemlösung liegt also darin, für &lt;em&gt;alle&lt;/em&gt; virtual Hosts QUIC zu aktivieren, die sich eine IP-Adressen / Port / Zertifikats-Kombination teilen.&lt;/strong&gt; In meinem Fall habe ich auf dem Proxy-Nginx QUIC also auch für music.metalhead.club (und watch.metalhead.club) aktiviert. Das Problem war damit gelöst.&lt;/p&gt;</description></item><item><title>Globale DNS-Auflösung durch Verzicht auf CNAMES beschleunigen</title><link>https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/</link><pubDate>Thu, 19 Jun 2025 10:27:52 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/</guid><description>&lt;p&gt;Wie beispielsweise in meinem Artikel &amp;ldquo;&lt;a href="https://thomas-leister.de/mastodon-media-storage-cdn/"&gt;Ein eigenes kleines CDN für meine Mastodon Instanz metalhead.club&lt;/a&gt;&amp;rdquo; gezeigt, nutze ich gerne CNAMEs, um meine DNS-Einträge zu organisieren. Üblicherweise lege ich für jeden Host einen DNS-Record an, der seinen Hostnamen auf die IP mappt. Dann verwende ich einen oder mehrere CNAME-Einträge, um gewisse (Sub-) Domains je nach Dienst und Verwendungszweck auf diese CNAMES zu verlinken. Das hilft bei der Übersicht und kann die Organisation erleichtern - vor allem, wenn einmal IP-Adressen für Hosts ausgetauscht werden müssen. Dank der Verkettung der Einträge muss nur das letzte Mapping von Server-Hostname zu IP angepasst werden, wenn die IP-Adresse des Zielhosts sich einmal ändern sollte.&lt;/p&gt;
&lt;p&gt;Anhand des Beispiels aus dem vorher erwähnten Artikel kann eine CNAME-Kette beispielsweise so aussehen:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[thomas@thomas-nb]~% dig media.metalhead.club
;; ANSWER SECTION:
media.metalhead.club. 1800 IN CNAME metalheadclub-media.cdn.650thz.de.
metalheadclub-media.cdn.650thz.de. 21600 IN CNAME s3.650thz.de.
s3.650thz.de. 3600 IN A 5.1.72.141
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Es wird also zunächst media.metalhead.club aufgelöst, dann metalheadclub-media.cdn.650thz.de und schließlich s3.650thz.de. Erst dann steht die IP-Adresse für den Zielhost fest. Das gefällt mir im Sinne der Ordnung, weil die Gliederung für mich im Kopf Sinn ergibt. Wie ich allerdings bei der Analyse meines &lt;a href="https://thomas-leister.de/mastodon-media-storage-cdn/"&gt;kleinen CDNs für metalhead.club&lt;/a&gt; feststellen müsste, ist das für die Performance nicht besonders förderlich. Denn der Gebrauch von CNAMES hat aber auch einen bedeutenden Nachteil: Die Zeit für eine DNS-Auflösung verlängert sich mit jedem CNAME in der Kette bis zur IP-Adresse.&lt;/p&gt;
&lt;p&gt;Allein die Auflösung der zweiten Zeile aus der &amp;ldquo;Answer Section&amp;rdquo; bedeutet beispielsweise: Erst .de auflösen (DNS-Rootserver befragen), dann .de Nameserver nach 650thz.de befragen, dann 650thz.de Nameserver nach cdn befragen, schließlich die cdn-Nameserver nach metalhead.club-media befragen. Erst dann steht fest: Es muss s3.650thz.de aufgelöst werden. Das Spiel beginnt erneut, bis am Ende schließlich die IP-Adresse feststeht.&lt;/p&gt;
&lt;p&gt;Es dürfte klar sein, dass diese Kette aus zwei CNAME-Einträgen durchaus Zeit braucht, bis sie vom DNS-Resolver des Nutzers aufgelöst ist. Für regionale Nutzer, die sich in Reichweite dieser Nameserver befinden, fällt der Aufwand nicht so sehr ins Gewicht, da das Auflösen jedes Kettenglieds in der Regel nur einige zehntel Millisekunden braucht.&lt;/p&gt;
&lt;p&gt;Anders sieht es aber für Nutzer aus, die (wie im Fall des metalhead.club) beispielsweise aus den USA oder aus Australien auf Nameserver in Europa zugreifen müssen. Denn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rootserver: Global&lt;/li&gt;
&lt;li&gt;.de-Nameserver: Global&lt;/li&gt;
&lt;li&gt;650thz.de: Regional, EU&lt;/li&gt;
&lt;li&gt;cdn.650thz.de: Regional, EU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In meiner Kette sind 4 Zonen auf 4 (virtuellen) Nameservern involviert. Jeder muss einzeln befragt werden und nur zwei davon sind global aufgestellt, d.h. sie sind weltweit mit minimalen Latenzzeiten erreichbar. Alle regionalen Server sind für alle, die sich nicht in der EU aufhalten, nur mit deutlich erhöhten Laufzeiten erreichbar. Dass gleich 2x (aus ihrer Sicht &amp;ldquo;langsame&amp;rdquo;) regionale DNS-Nameserver im Fall eines US-Nutzers abgefragt werden müssen, sorgt dann dafür, dass sich die erhöhten Latenzen sehr schnell aufaddieren.&lt;/p&gt;
&lt;p&gt;Aus Sicht eines US-Nutzers oder eines Nutzers aus Asien ist es also besonders wichtig, dass Auflösungsketten möglichst kurz gehalten werden. Besonders deutlich wird das Bild, wenn man ein Tool wie z.B. &lt;a href="https://globalping.io"&gt;globalping.io&lt;/a&gt; nutzt und sich die globalen Auflösungszeiten im DNS-Modus einmal ansieht:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/images/globalping-unoptimized.png" alt="Screenshot globalping.io ohne Optimierung"&gt;&lt;/p&gt;
&lt;p&gt;Während EU-Nutzer media.metalhead.club (im Screenshot media-old.metalhead.club) nach etwa 0,004 - 0,070 Sekunden aufgelöst bekommen, muss sich ein Japaner deutlich länger gedulden: Nämlich ca 1,5 Sekunden! Erst dann kann sein Browser anfangen, Kontakt zum endgültigen Zielserver aufzunehmen.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Das gilt selbstverständlich nur für die erste DNS-Anfrage nach Ablauf der TTL. Kurz darauf folgende Anfragen werden vom DNS-Resolver üblicherweise aus dem Cache beantwortet, sodass diese um Größenordnungen schneller beantwortet werden. Nach Ablauf der TTL dauert der erste Request allerdings wieder 1,5 Sekunden! Allerdings kann bei den wenigen Nutzern in einigen Regionen nicht angenommen werden, dass diese einen gemeinsamen DNS-Resolver nutzen. Daher ist es mir wichtig, dass auch ungecachte Requests zügig beantwortet werden.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Also habe ich mich entschieden, Auf CNAME-Ketten ganz zu verzichten. Und das ging so:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In der metalhead.club Zone habe ich die Subdomain media.metalhead.club direkt an den GeoIP-Nameserver von Scaleway delegiert&lt;/li&gt;
&lt;li&gt;Der Scaleway-Nameserver löst dann direkt zu einer IP-Adresse auf, die zur Region des Nutzers passt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es sind also keine CNAMEs mehr an der Auflösung beteiligt: Die gesamte Auflösung basiert nur noch auf Zonendelegation.&lt;/p&gt;
&lt;p&gt;Das Ganze ließe sich durch folgende Maßnahmen nochmal verbessern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keine Trennung von &amp;ldquo;normalem&amp;rdquo; DNS-Nameserver (Core-Networks.de) und GeoIP Nameserver (Scaleway): Dann könnte auf die &amp;ldquo;media&amp;rdquo;-Zonendelegation verzichtet werden&lt;/li&gt;
&lt;li&gt;Globale Erreichbarkeit aller Nameserver (ist allerdings ein nicht unbedeutender Kostenfaktor, außerdem neuer DNS-Anbieter erforderlich)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aber auch ohne diese beiden Verbesserungen konnte ich alleine durch den Verzicht auf CNAMES auf Auflösungszeit deutlich verkürzen, wie dieser Screenshot zeigt:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/images/globalping-optimized.png" alt="Screenshot von globalping.io: Zeigt Auflösungszeit von Japan. Jetzt 676 ms statt 1,884 Sekunden"&gt;&lt;/p&gt;
&lt;p&gt;Das ist doch schon viel besser: Nach der Optimierung durch Verzicht auf CNAMES dauert die Auflösung von media.metalhead.club in diesem Fall nur noch rund 0,7 Sekunden statt 1,9 Sekunden!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Übrigens: Die Hauptdomain metalhead.club löst technisch bedingt sowieso gleich auf IP-Adressen auf. Hier ist keine Optimierung nötig gewesen.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Ein eigenes kleines CDN für meine Mastodon-Instanz metalhead.club</title><link>https://thomas-leister.de/mastodon-media-storage-cdn/</link><pubDate>Thu, 19 Jun 2025 08:11:04 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/mastodon-media-storage-cdn/</guid><description>&lt;p&gt;Seit der großen Twitter-Welle, die das Mastodon-Netzwerk bzw. das Fediverse im weiteren Sinne im Herbst und Winter 2022 geflutet hat, spielen internationale User für den metalhead.club eine größere Rolle. Der Service ist vollständig in Deutschland gehostet und das war bis vor kurzem auch noch so. Doch mit der steigenden Zahl von internationalen Mitgliedern kommen auch neue Herausforderungen: Beispielsweise die der zügigen Content-Auslieferung.&lt;/p&gt;
&lt;p&gt;Solange sich die Nutzer hauptsächlich in Deutschland und Europa befinden, sind die Latenzzeiten zum &amp;ldquo;Full Metal Server&amp;rdquo; in Frankfurt gering. Anders sieht es aber beispielsweise für die Nutzer aus Kanada, aus den USA und aus Australien aus, von denen es eine nicht zu unterschätzende Zahl im metalhead.club gibt. Für diese Nutzer war die Nutzung von metalhead.club manchmal ein kleines Geduldsspiel, denn vor allem Videos und größere Bilder erschienen nur mit einer kleinen Verzögerung auf der Website. Ich kann die Situation nur im Browser simulieren, doch bereits ein Ping von mehr als 200 ms verdirbt einem an so mancher Stelle den Spaß am Durchscrollen der Timeline.&lt;/p&gt;
&lt;p&gt;Vor kurzem hat mich ein US-Nutzer auf ein konkretes Problem beim Abspielen eines Videos innerhalb der USA aufmerksam gemacht. Das iFixit Video konnte von ihm nur mit ständigen Unterbrechungen gestreamt werden. Grund für mich, mir endlich einmal Gedanken zu einem CDN zu machen - einem Content Delivery Network.&lt;/p&gt;
&lt;p&gt;Sinn und Zweck eines solchen Netzwerks ist, die Inhalte, die einem Nutzer einer Plattform oder Website zur Verfügung stehen, gewissermaßen näher an den Nutzer zu rücken. Das ist nicht nur metaphorisch gemeint, sondern tatsächlich auch eine physische Angelegenheit: Selbst wenn man annimmt, dass durch Internetknotenpunkte, Signalrepeater und sonstiges Equipment keine zusätzlichen Latenzen auf der Strecke von hier in die USA ins System eingebracht werden, ist das Licht in den Meereskabeln bekanntermaßen nicht unendlich schnell. Allein dieser Umstand führt dazu, dass man die Webinhalte tatsächlich (physisch) näher am Kunden anbieten muss. Ein CDN besteht also aus vielen Datacenter-Standorten bzw. Servern, die in der Nähe der Nutzer installiert werden - idealerweise in Ballungsräumen, wo man zusätzlich von einer gut ausgebauten Netzinfrastruktur profitieren kann. Je nach Use Case in gewissen Weltregionen oder sogar auf der ganzen Welt. Die Inhalte werden auf alle (oder einige) der Server gespiegelt - je nach Bedarf. Ein US-Nutzer wird beim Anfordern der Inhalte nicht an einen EU-Server weitergeleitet, sondern ruft die Inhalte automatisch von einem Server in seiner Nähe (einem US-Datacenter) ab. Dieser Vorgang bleibt vor dem Nutzer idR
transparent. Um zu erkennen, von welchem Standort aus ein Inhalt abgerufen wird (und welcher der zuständige Server ist), gibt es zwei Verfahren, die sich etabliert haben:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Anycast-basierte CDNs&lt;/li&gt;
&lt;li&gt;&amp;ldquo;GeoIP&amp;rdquo;-basierte CDNs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="anycast-basierte-cdns"&gt;Anycast-basierte CDNs&lt;/h2&gt;
&lt;p&gt;Ersterer CDN-Typ ist der &amp;ldquo;Goldstandard&amp;rdquo; und ermöglicht ein sehr elegantes (und genaues) Routing: Im Prinzip wird mehreren weltweit verstreuten Servern dieselbe öffentliche IP-Adresse zugeordnet. Welcher Server dann bei einer Anfrage tatsächlich angesprochen wird, wird durch das Routingprotokoll BGP entschieden. Dieses legt gewissermaßen die Pfade fest, die ein IP-Paket nehmen muss, um ans Ziel zu gelangen. Meistens gibt es mehrere Wege, ans Ziel zu kommen. Welcher mit den geringsten Kosten (oder der geringsten Latenz!) verbunden ist, weiß das Routingsystem. Insofern gibt man die Arbeit an das Routingsystem ab und verlässt sich darauf, dass es den schnellsten Weg durch&amp;rsquo;s Internet findet. Denn dafür ist es da - dafür wurde es entwickelt. Der Weg muss nicht immer der geografisch kürzeste sein. Wenn die Latenz auf Strecke A zu Server A kleiner ist als Strecke B zu Server B, werden die IP-Pakete automatisch zu Server A geleitet. Dieser antwortet dann. Die übrigen Server, die unter derselben IP-Adresse erreichbar sind, bekommen von der Anfrage nichts mit. So ein Anycast steckt übrigens auch hinter allen global verfügbaren DNS-Resolvern wie z.B. Google DNS, Quad9 oder Cloudflare DNS. Die IP-Adresse ist immer dieselbe - doch im Hintergrund wird man zum nächstgelegenen Server weitergeleitet, ohne etwas davon mitzubekommen.&lt;/p&gt;
&lt;h2 id="geoip-basierte-cdns"&gt;GeoIP-basierte CDNs&lt;/h2&gt;
&lt;p&gt;Hinter dem Begriff GeoIP versteckt sich eine große Datenbank - eine GeoIP-Datenbank, wie sie beispielsweise von Unternehmen wie &lt;a href="https://www.maxmind.com/en/geoip-databases"&gt;MaxMind&lt;/a&gt; angeboten werden (und wie sie beispielsweise in meinem Projekt &lt;a href="https://watch.metalhead.club"&gt;watch.metalhead.club&lt;/a&gt; zum Einsatz kommt). Die Datenbank wird aus öffentlich verfügbaren Routinginformationen gespeist und speichert, welche IP-Adressbereiche (und damit ASNs) welchem Unternehmen zugeordnet sind. Sobald die Inhaberschaft einer IP-Adresse klar ist, lassen sich über das zugeordnete Unternehmen Rückschlüsse auf das geografische Nutzungsgebiet ziehen. IPv6-Adressen, die dem &lt;code&gt;2003::/19&lt;/code&gt; Netz kommen (also &lt;code&gt;2003::&lt;/code&gt; bis &lt;code&gt;2003:1fff:ffff:ffff:ffff:ffff:ffff:ffff&lt;/code&gt;) gehören beispielsweise der Deutschen Telekom. Damit ist auch klar, woher ein Benutzer kommt, der so eine IP-Adresse hat und einen Inhalt damit anfragt: Aus Deutschland. GeoIP Datenbanken speichern diese Zusammenhänge, sodass man sie dann befragen kann: &amp;ldquo;Woher kommt ein Nutzer mit der IP-Adresse xxx?&amp;rdquo;. Die Datenbank liefert dann eine mehr oder weniger genaue Antwort.&lt;/p&gt;
&lt;h3 id="die-genauigkeit-von-geoip-basierten-cdns"&gt;Die Genauigkeit von GeoIP-basierten CDNs&lt;/h3&gt;
&lt;p&gt;Meine Erfahrung mit den kostenlosen Angeboten solcher Datenbanken hat mir gezeigt: Auf Länderebene funktioniert das noch ganz gut - aber bis auf die Stadt genau kann man Nutzer meistens nicht verorten. Und das ist vielleicht auch ganz gut so. Es gibt bessere und schlechtere Datenbanken. Die genauen Datenbanken befinden sich meistens hinter einem kostspieligen Abomodell. Denn: GeoIP-Datenbanken müssen gepflegt werden. IP-Adressen bzw. Subnetze werden vor allem in Zeiten von IPv4-Adressmangel gehandelt und verkauft und an andere Anbieter verkauft. Ein Adressbereich, der gerade noch einem afrikanischen Mobilfunkanbieter gehört hat, kann morgen schon einem kleinen asiatischen Unternehmen gehören. Werden die Datenbanken nicht regelmäßig gepflegt, sind die Informationen veraltet und wertlos.&lt;/p&gt;
&lt;p&gt;Und genau hier liegt der große Nachteil von GeoIP. Während sich Anycast-basierte CDNs darauf stützen, dass Routingsysteme bereits die kürzesten Strecken (im Sinne von Latenz) kennen, verlässt man sich bei GeoIP auf weitere gesammelte Informationen. Die Zuordnung IP-Adresse &amp;lt;=&amp;gt; Besitzer ist nicht schwer zu ermitteln und öffentlich. Die Schwierigkeit dürfte viel eher darin liegen, Art der Verwendung und Verwendungsregion für einen Besitzer korrekt zu ermitteln. Es kann sich schließlich um ein kleines Unternehmen handeln, aber auch im einen ISP, der die Adressen nur an seine Kunden weiterverteilt. Wie so eine Verteilung stattfindet und wie groß das geografische Verwendungsgebiet ist, weiß dann nur der Besitzer bzw. ISP. Um diese Zuordnung akkurater zu gestalten, können zusätzliche Trackinginformationen verwendet werden. So könnten vor allem dank Smartphones beispielsweise GPS-Standpunkt und verwendete IP-Adresse von einem Onlinewerbeunternehmen kombiniert werden, um den Aufenthaltsort eines Nutzers genauer zu bestimmen. Werden diese GPS-Informationen dann an den GeoIP-Anbieter verkauft, kann dieser den Verwendungsort der IP-Adresse viel genauer bestimmen. Aber das ist eine andere Geschichte &amp;hellip;&lt;/p&gt;
&lt;p&gt;Wie läuft nun die Weiterleitung eines Nutzers an den richtigen Server ab, wenn kein Anycast genutzt wird? Ganz einfach: Über den Verzeichnisdienst des Internets - das DNS.&lt;/p&gt;
&lt;p&gt;Will man ein GeoIP-basiertes CDN bauen, braucht man DNS-Server, der dem Nutzer die passende IP-Adresse für seine Regio zurückgibt. Bei jeder Anfrage an den DNS-Service wird die IP-Adresse ermittelt und im Hintergrund der passende Zielhost basierend auf einer GeoIP-Datenbank ermittelt. Ich schreibe bewusst &amp;ldquo;die IP-Adresse&amp;rdquo;, denn welche IP-Adresse genau untersucht wird, kommt darauf an &amp;hellip;&lt;/p&gt;
&lt;h3 id="resolver-ip-oder-edns-client-subnet-ecs"&gt;Resolver-IP oder EDNS Client Subnet (ECS)?&lt;/h3&gt;
&lt;p&gt;Man könnte nun meinen, es würde die Adresse des anfragenden Nutzers untersucht, beispielsweise die öffentliche IP-Adresse des Smartphones oder des Laptops - dem ist aber nicht so. Denn: Der Nameserver, der für meine Domain z.B. metalhead.club zuständig ist, bekommt diesen Client (fast) nie zu Gesicht. Es sind nämlich die &lt;em&gt;DNS-Resolver&lt;/em&gt;, die für den Client die korrekte IP für einen Service anfragen - &lt;em&gt;nicht die Clients&lt;/em&gt; selbst. Alles, was der GeoIP-Nameserver also auswerten kann, ist die IP-Adresse des Resolvers.&lt;/p&gt;
&lt;p&gt;In vielen Fällen hat man so zwar nicht den Nutzer selbst geortet, ist aber in der Lage, seinen DNS-Resolver zu identifizieren. In den meisten Fällen ist das bei Privatkunden der Standard-DNS-Resolver des Kunden-ISPs, also beispielsweise der Deutschen Telekom. Der Standort des DNS-Resolvers stimmt in den meisten Fällen also ungefähr mit dem Standort des Kunden überein - zumindest, wenn man die Sache auf Länderebene betrachtet. Sieht mein Nameserver also eine IP, die einem Deutsche Telekom Resolver zugeordnet werden kann, kann ich idR. davon ausgehen, dass der eigentlich anfragende Nutzer ebenfalls aus Deutschland kommt.&lt;/p&gt;
&lt;p&gt;Dann gibt es da aber noch Sonderfälle: Nämlich global verfügbare und ISP-unabhängige, offene DNS-Resolver. Beispielsweise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare DNS&lt;/li&gt;
&lt;li&gt;Google DNS&lt;/li&gt;
&lt;li&gt;Quad9&lt;/li&gt;
&lt;li&gt;DNS4EU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Diese lassen häufig keine Rückschlüsse auf ein bestimmtes Land zu. Damit man den eigentlich anfragenden Kunden hinter dem DNS-Request dennoch ungefähr orten kann, gibt es eine EDNS-Erweiterung namens &amp;ldquo;ECS&amp;rdquo; (EDNS Client Subnet), die es erlaubt, zusätzliche Informationen an meinen Nameserver zu schicken: Nämlich das Subnetz des ursprünglichen Nutzers. Der Resolver kennt diese Client-IP naturgemäß und leitet sie in anonymisierter Form (nur Subnetz statt genauer IP-Adresse) an meinen Nameserver weiter. Dieser kann nun das per ECS weitergeleitete Subnetz statt der Resolver-IP für seine GeoIP-Datenbank verwenden und erhält ein präziseres Ergebnis zum geografischen Ursprung der Anfrage.&lt;/p&gt;
&lt;p&gt;ECS wird nicht von allen Public Resolvern unterstützt - von DNS4EU wird ECS beispielsweise noch nicht unterstützt - auch, wenn das Feature bei dem Betreiber auf der Todo-Liste steht:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DNS4EU currently does not support EDNS, but we want to add support for this feature. Unfortunately, we do not have a timeline on when this should be done. Please check the project page &lt;a href="https://www.joindns4.eu/learn"&gt;https://www.joindns4.eu/learn&lt;/a&gt; for updates.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="was-also-nutzen-cdn-provider-oder-selbst-hosten"&gt;Was also nutzen? CDN-Provider oder selbst hosten?&lt;/h2&gt;
&lt;p&gt;Anycast-Systeme sind teuer. Sehr teuer. Und für jemanden wie mich nicht umzusetzen. Denn dazu müsste man einen eigenen IP-Adressbereich besitzen, für den man selbst BGP Routen definieren kann. Somit war das Thema für mich schnell erledigt. Anycast? Betreibe ich nicht selbst. Nun gibt es da aber noch eine andere Möglichkeit: Schließlich kann man sich ja in bestehende Anycast-Systeme einmieten oder CDNs einfach mieten. Auch das mieten von Anycast-IPs ist kostspielig und wird von den Hostern, die ich nutze bzw. nutzen will, nicht in der Form angeboten.&lt;/p&gt;
&lt;p&gt;Frei mietbare CDNs gibt es jedoch wie Sand am Meer: BunnyCDN, Akamai, Cloudflare, KeyCDN, &amp;hellip; sie sind nur einige populäre Beispiele. Oftmals werden diese CDNs auch in Kombination mit DNS-Services, S3 Speicher oder DDoS-Schutz verkauft. Mit dabei sind auch einige Angebote, die man sich als kleiner Plattformbetreiber im Internet gut leisten kann. Mein Interesse galt vor allem BunnyCDN. Mit ca. 20-25 € im Monat könnte ich die etwa 2.5 TB an metalhead.club Medien im Monat weltweit ausspielen lassen. Doch es gibt zwei Haken, die mich derzeit noch daran stören:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ich muss die TLS-Terminierung aus der Hand geben und die Medien fremd hosten lassen. Meine Nutzer verlassen sich allerdings darauf, dass ich alle Daten in meiner eigenen Hand habe&lt;/li&gt;
&lt;li&gt;Das Ökostrom-Problem: Es ist einfacher geworden, Serverhoster zu finden, die mit reinem Ökostrom arbeiten. Bei CDN-Anbietern sieht es leider noch nicht so gut aus: Nur Cloudflare &lt;a href="https://blog.cloudflare.com/de-de/cloudflare-committed-to-building-a-greener-internet/"&gt;scheint 100 % Ökostrom einzusetzen&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Damit fällt also auch das Einmieten in einem bestehenden CDN (erst einmal?) weg.&lt;/p&gt;
&lt;h2 id="ein-eigenes-geo-ip-basiertes-cdn-basteln"&gt;Ein eigenes Geo-IP basiertes CDN basteln&lt;/h2&gt;
&lt;p&gt;Bleibt also nur: Selbst basteln!&lt;/p&gt;
&lt;p&gt;Meine Ansprüche sind relativ niedrig. Mir ist natürlich bewusst, dass ein eigenes GeoIP-basiertes CDN schnell an seine Grenzen bzw. Effizienz, Genauigkeit und Performance kommt. Ganz besonders dann, wenn man - wie ich - vor hat, nur sehr wenig Geld dafür zu investieren. Mir geht es in erster Linie darum, die Nutzererfahrung für einige meiner metalhead.club Member &lt;em&gt;besser&lt;/em&gt; zu machen. Die perfekte Lösung werde ich auf diese Art und Weise nicht erreichen.&lt;/p&gt;
&lt;p&gt;Da die Versorgungssituation innerhalb Europas in Ordnung ist und Latenzmessungen über Online-Tools (dazu später mehr!) ein zufriedenstellendes Ergebnis geliefert haben, habe ich mich vor allem zwei Standorten gewidmet: Dem amerikanischen Kontinent und dem asiatischen. Damit wäre mit insgesamt 3 Medienservern für media.metalhead.club der ganze Globus grob abgedeckt.&lt;/p&gt;
&lt;p&gt;Über das &lt;a href="https://aaronstanek.com/projects/ping-latency-map"&gt;&amp;ldquo;Ping Simulation&amp;rdquo; Tool&lt;/a&gt; habe ich mir angesehen, mit welchen Latenzen ich global in etwas rechnen kann, wenn ich die Server so verteile, wie ich es mir vorgestellt habe. Nämlich so, wie es mir der Serverhoster Hetzner durch seine Cloud erlaubt. Einen Hetzner-Account habe ich bereits und kenne mich mit den Produkten aus - zudem erfüllen sie meine Anforderungen (Deutsches Unternehmen, DSGVO-konform, 100 % Ökostrom). Und auch, was die Standorte angeht, hat Hetzner genau das im Angebot was ich will. Denn es gibt neben europäischen Rechenzentren auch zwei in den USA und eines in Singapur.&lt;/p&gt;
&lt;p&gt;Die Simulation ergab, dass ich weite Teile der Welt mit etwa 100 ms oder weniger erreichen kann. Natürlich auch immer abhängig vom lokalen Provider, aber das Gesamtbild war einigermaßen gleichmäßig und nur an wenigen Standorten waren die Paketlaufzeiten deutlich höher als 150 ms. Also habe ich bei Hetzner kurzerhand einen Cloudserver in Ashburn (VA, USA) und einen in Singapur gebucht.&lt;/p&gt;
&lt;h2 id="implementierung"&gt;Implementierung&lt;/h2&gt;
&lt;p&gt;Was die vServer angeht, habe ich zu den kleinsten und günstigsten Modellen gegriffen, die Hetzner anbietet. Ich wollte klein starten und das Ganze erst einmal ausprobieren. Wie sich gezeigt hat, reichen die CPX11 Cloudserver bisher völlig aus. Sie müssen nur statische Dateien cachen und ausliefern - mehr nicht.&lt;/p&gt;
&lt;h3 id="nginx-caching-proxy"&gt;Nginx Caching Proxy&lt;/h3&gt;
&lt;p&gt;Auf den Servern läuft nur eine Nginx-Instanz, die&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;hellip; Anfragen an media.metalhead.club entgegennimmt&lt;/li&gt;
&lt;li&gt;nachsieht, ob die angeforderten Inhalte schon lokal vorliegen&lt;/li&gt;
&lt;li&gt;(falls nicht, werden diese vom original S3 Bucket angefragt und gecached)&lt;/li&gt;
&lt;li&gt;an den Client zurückliefert&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meine Konfiguration für Nginx:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;proxy_cache_path /tmp/nginx-cache-metalheadclub-media levels=1:2 keys_zone=s3_cache:10m max_size=30g
inactive=48h use_temp_path=off;
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name media.metalhead.club;
include snippets/tls-common.conf;
ssl_certificate /etc/acme.sh/media.metalhead.club/fullchain.pem;
ssl_certificate_key /etc/acme.sh/media.metalhead.club/privkey.pem;
client_max_body_size 100M;
# Register backend URLs
set $minio_backend &amp;#39;https://metalheadclub-media.s3.650thz.de&amp;#39;;
root /var/www/media.metalhead.club;
location / {
access_log off;
try_files $uri @minio;
}
#
# Own Minio S3 server (new)
#
location @minio {
limit_except GET {
deny all;
}
resolver 9.9.9.9;
proxy_set_header Host &amp;#39;metalheadclub-media.s3.650thz.de&amp;#39;;
proxy_set_header Connection &amp;#39;&amp;#39;;
proxy_set_header Authorization &amp;#39;&amp;#39;;
proxy_hide_header Set-Cookie;
proxy_hide_header &amp;#39;Access-Control-Allow-Origin&amp;#39;;
proxy_hide_header &amp;#39;Access-Control-Allow-Methods&amp;#39;;
proxy_hide_header &amp;#39;Access-Control-Allow-Headers&amp;#39;;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-meta-server-side-encryption;
proxy_hide_header x-amz-server-side-encryption;
proxy_hide_header x-amz-bucket-region;
proxy_hide_header x-amzn-requestid;
proxy_ignore_headers Set-Cookie;
proxy_pass $minio_backend$uri;
# Caching to avoid S3 access
proxy_cache s3_cache;
proxy_cache_valid 200 304 48h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_revalidate on;
expires 1y;
add_header Cache-Control public;
add_header &amp;#39;Access-Control-Allow-Origin&amp;#39; &amp;#39;*&amp;#39;;
add_header X-Cache-Status $upstream_cache_status;
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy &amp;#34;default-src &amp;#39;none&amp;#39;; form-action &amp;#39;none&amp;#39;&amp;#34;;
add_header X-Served-By &amp;#34;cdn-us.650thz.de&amp;#34;;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;Übrigens: Das Hinzufügen des &lt;code&gt;X-Served-By&lt;/code&gt; Headers erleichtert das Debuggen bzw. Überprüfen der Funktion. Für jedes Bild, das von media.metalhead.club geladen wird, lässt sich so sehr einfach feststellen, welcher der drei Hosts die Datei denn tatsächlich übertragen hat. Einsehbar ist der Header beispielsweise in den Entwicklertools jedes Webbrowsers.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="geoip-basierte-dns-zonen"&gt;GeoIP-basierte DNS-Zonen&lt;/h3&gt;
&lt;p&gt;Fehlt nun noch der GeoIP-Anteil, damit die metalhead.club Member an den jeweils passenden Server weitergeleitet werden. An dieser Stelle hätte ich einen eigenen GeoIP-/GeoDNS-fähigen Nameserver konfigurieren können (wie ich es in einem eigenen Kundenprojekt schon getan habe), doch für mein Experiment wollte ich nicht zu viel Aufwand betreiben. Also habe ich mich etwas umgesehen und bei Scaleway ein sehr interessantes Angebot gefunden. Dort kann man nämlich Nameserver mieten, die zum einen das GeoIP Feature beherrschen und sich zum anderen auch mit externen Domains nutzen lassen (also Domains, die man nicht bei Scaleway gebucht hat). Je nach Nutzung kostet ein solcher Service nur wenige Cent bis niedrige einstellige Eurobeträge für meinen Fall. Genau kann ich es sagen, wenn mein CDN eine Weile gelaufen ist ;-)&lt;/p&gt;
&lt;p&gt;Allerdings wollte ich Scaleway nicht meine 650thz.de Root Zone überlassen. Diese sollte nach wie vor von Core-Networks.de gehostet werden. Stattdessen habe ich eine eigene Zone &amp;ldquo;cdn.650thz.de&amp;rdquo; angelegt und die Scaleway-Nameserver als autoritative Nameserver festgelegt. Zuvor musste allerdings die Eigentümerschaft von cdn.650thz.de gegenüber Scaleway bewiesen werden. Die NS-Einträge habe ich also noch nicht gesetzt, sondern zuerst einen Verifizierungseintrag in der metalhead.club Zone angelegt:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;_scaleway-challenge IN TXT 3600 &amp;#34;verifizierungs-string&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nach ca. 30 Minuten war bestätigt, dass ich die Kontrolle über die Domain habe und ich konnte anfangen, die Scaleway-Nameserver für cdn.650thz.de in der 650thz.de Zone bei Core-Networks.de einzutragen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cdn 86400 NS ns0.dom.scw.cloud.
cdn 86400 NS ns1.dom.scw.cloud.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(den &lt;code&gt;_scaleway-challenge&lt;/code&gt; Eintrag habe ich wieder gelöscht)&lt;/em&gt;&lt;/p&gt;
&lt;div class="tip"&gt;
Übrigens: Die 650thz.de Domain nutze ich für sämtliche Infrastruktur im Hintergrund, da metalhead.club zum 650thz.de Projekt gehört. Lasst euch davon nicht verwirren.
&lt;/div&gt;
&lt;p&gt;In der &lt;code&gt;cdn.650thz.de&lt;/code&gt; Zone habe ich dann schließlich den GeoIP-Eintrag für &lt;code&gt;metalheadclub-media.cdn.650thz.de&lt;/code&gt; hinzugefügt:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/mastodon-media-storage-cdn/images/scaleway-geoip-record.png" alt="Screenshot Eingabemaske DNS bei Scaleway"&gt;&lt;/p&gt;
&lt;p&gt;Der default-Eintrag zeigt auf &lt;code&gt;s3.650thz.de&lt;/code&gt; - den bisherigen S3-Medienserver in Frankfurt. Sollte jedoch ein Clientstandort in Nord- oder Südamerika erkannt werden, wird stattdessen der CDN-Server &lt;code&gt;cdn-us.650thz.de&lt;/code&gt; genutzt. Wird ein Standort in Asien oder im pazifischen Raum erkannt, wird auf den CDN-Server &lt;code&gt;cdn-ap.650thz.de&lt;/code&gt; verwiesen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Essentiell ist übrigens der abschließe Punkt hinter jedem der FQDNs in der Eingabemaske!&lt;/strong&gt; Wird kein Punkt gesetzt, wird ein Eintrag in derselben Zone referenziert - das kann nicht funktionieren.&lt;/p&gt;
&lt;div class="tip"&gt;
Die Verkettung von DNS-Records mittels CNAME hat sich im Nachhinein übrigens als ungünstig für weit entfernte Benutzer herausgestellt. Mehr dazu unter &lt;a href="https://thomas-leister.de/mastodon-media-storage-cdn/#weitere-verbesserungen-dns-optimierung"&gt;&amp;ldquo;Weitere Verbesserungen: DNS-Optimierung&amp;rdquo;&lt;/a&gt;
&lt;/div&gt;
&lt;h3 id="umschaltung-auf-cdn-betrieb"&gt;Umschaltung auf CDN-Betrieb&lt;/h3&gt;
&lt;p&gt;Um nun alle Anfragen an media.metalhead.club mit dem passenden Medienserver zu beantworten, war noch noch eine Änderung nötig: Der Eintrag für &lt;code&gt;media.metalhead.club&lt;/code&gt; musste im &lt;code&gt;metalhead.club&lt;/code&gt; Zonefile auf &lt;code&gt;metalheadclub-media.cdn.650thz.de&lt;/code&gt; verweisen:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;media IN CNAME 3600 metalheadclub-media.cdn.650thz.de.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;(auch hier wieder auf den abschließenden Punkt achten!)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Um das CDN nach der DNS-Umstellung (und der zuvor eingetragenen TTL) zu testen, habe ich kurzerhand von jedem der Server aus einen &lt;code&gt;nslookup&lt;/code&gt; auf &lt;code&gt;media.metalhead.club&lt;/code&gt; gemacht:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Von s3.650thz.de aus: Gibt eigene IP zurück&lt;/li&gt;
&lt;li&gt;Von cdn-us.650thz.de aus: Gibt eigene IP zurück&lt;/li&gt;
&lt;li&gt;Von cdn-ap.650thz.de aus: Gibt eigene IP zurück&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Die dynamische Zuteilung funktionierte also!&lt;/p&gt;
&lt;h2 id="wie-gut-funktioniert-das"&gt;Wie gut funktioniert das?&lt;/h2&gt;
&lt;p&gt;Um nun noch ein breiteres Bild zu bekommen, habe ich noch weitere Tests durchgeführt, zum Beispiel mit folgenden Tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CDN HTTP Response Time Test: &lt;a href="https://tools.keycdn.com/performance"&gt;https://tools.keycdn.com/performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CDN Test mit IPs: &lt;a href="https://www.uptrends.com/tools/cdn-performance-check"&gt;https://www.uptrends.com/tools/cdn-performance-check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;BunnyCDN Ping Test: &lt;a href="https://tools.bunny.net/latency-test"&gt;https://tools.bunny.net/latency-test&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CDNPerf Test: &lt;a href="https://www.cdnperf.com/tools/cdn-latency-benchmark"&gt;https://www.cdnperf.com/tools/cdn-latency-benchmark&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Auch metalhead.club User habe ich dazu befragt. Ein Nutzer aus Australien hatte zuvor diese Ping-Zeiten zu media.metalhead.club:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;64 bytes from 5.1.72.141: icmp_seq=0 ttl=38 time=369.765 ms
64 bytes from 5.1.72.141: icmp_seq=1 ttl=38 time=350.830 ms
64 bytes from 5.1.72.141: icmp_seq=2 ttl=38 time=469.047 ms
64 bytes from 5.1.72.141: icmp_seq=3 ttl=38 time=379.882 ms
64 bytes from 5.1.72.141: icmp_seq=4 ttl=38 time=400.998 ms
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;(also im Mittel knapp 400 ms!)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Danach waren die Latenzen deutlich niedriger, weil er korrekterweise zu dem US-Server statt dem EU-Server weitergeleitet wurde:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;64 bytes from 5.223.63.2: icmp_seq=0 ttl=50 time=150.384 ms
64 bytes from 5.223.63.2: icmp_seq=1 ttl=50 time=230.931 ms
64 bytes from 5.223.63.2: icmp_seq=2 ttl=50 time=360.083 ms
64 bytes from 5.223.63.2: icmp_seq=3 ttl=50 time=142.986 ms
64 bytes from 5.223.63.2: icmp_seq=4 ttl=50 time=138.416 ms
64 bytes from 5.223.63.2: icmp_seq=5 ttl=50 time=300.561 ms
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;(im Mittel 264 ms)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Nicht weltbewegend, aber dennoch bedeutend weniger. Dass dieser Benutzer trotzdem eine relativ hohe Latenz zum Medienspeicher hat, kommt daher, das die Strecke von Australien nach Singapur trotzdem nicht zu vernachlässigen ist. Hier zeigt mein kleines CDN gleich seine Schwäche: Durch nur 3 Standorte kann man keine Bestperformance erreichen - aber man kann immerhin eine kleine Verbesserung herbeiführen.&lt;/p&gt;
&lt;p&gt;Für einen anderen australischen User hat sich die Latenz &lt;em&gt;von durchschnittlich 346 ms auf 108 ms verbessert&lt;/em&gt;!&lt;/p&gt;
&lt;p&gt;Und für den User aus den USA, den ich zu Beginn erwähnt habe? Der konnte nach der Umstellung das fragliche Video problemlos und flüssig abspielen.&lt;/p&gt;
&lt;p&gt;Mit dem BunnyCDN Test habe ich einen Vorher-Nachher-Vergleich angestellt:&lt;/p&gt;
&lt;p&gt;Vorher:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/mastodon-media-storage-cdn/images/bunnycdn-screenshot-vorher.png" alt="BunnyCDN Screenshot zeigt Latenzzeiten vor der Umstellung auf ein CDN"&gt;&lt;/p&gt;
&lt;p&gt;Nachher:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/mastodon-media-storage-cdn/images/bunnycdn-screenshot-nachher.png" alt="BunnyCDN Screenshot zeigt Latenzzeiten nach der Umstellung auf ein CDN"&gt;&lt;/p&gt;
&lt;h2 id="ziel-erreicht"&gt;Ziel erreicht!&lt;/h2&gt;
&lt;p&gt;Für die meisten Standorte rund um die USA, Australien, Korea und Japan haben sich die Latenzzeiten deutlich verbessert. Europa ist wie erwartet unverändert. Es gibt aber auch einige unerwartete Ausreißer, die wohl einer falschen IP-Lokalisierung zum Opfer gefallen sind, beispielsweise in Singapur selbst (interessant!), in der Türkei, Indien, im US-Bundesstaat Texas und Chile.&lt;/p&gt;
&lt;p&gt;Selbstverständlich hängen die Ergebnisse auch stark damit zusammen, mit welchen IPs getestet wurde und wie die Anbindung vor Ort aussieht. Womöglich wurde die Türkei schon als &amp;ldquo;Asiatischer Raum&amp;rdquo; gewertet und der Standort, von dem aus in Texas getestet wurde, wurde womöglich als europäisch erkannt.&lt;/p&gt;
&lt;p&gt;Ich werde die Ergebnisse weiter beobachten. Da hier vor allem von Rechenzentren aus gemessen wurde (und nicht von Privathaushalten aus entsprechenden ISP-Bereichen), spiegeln die Ergebnisse womöglich nicht die ganze Wahrheit wider.&lt;/p&gt;
&lt;p&gt;Man kann sich also zurecht fragen: &amp;hellip;&lt;/p&gt;
&lt;h2 id="ist-auf-geoip-verlass"&gt;Ist auf GeoIP Verlass?&lt;/h2&gt;
&lt;p&gt;Da ich bei der Prüfung meines CDNs mit den oben erwähnten Test-Websites festgestellt habe, dass der kontaktierte Server nicht immer korrekt dem Teststandort entsprach (siehe Ergebnis des Singapur-Standorts!), habe ich meine Nutzer befragt. Denn die Ergebnisse dieser Web-Tools sind vermutlich nicht besonders genau. Ich schätze, dass der Mechanismus für Endkunden-Adressbereiche viel besser funktionieren als für Datacenter-Adressbereiche. Schließlich können für Privatkunden-IPs sehr viel einfacher GPS- und andere Standortinformationen &amp;ldquo;hinfusioniert&amp;rdquo; werden.&lt;/p&gt;
&lt;p&gt;Und tatsächlich: Die Nutzer bekamen in fast allen Fällen den korrekten Medienserver aufgrund ihrer Herkunft zugeteilt. Siehe: &lt;a href="https://metalhead.club/@thomas/114676148254141233"&gt;https://metalhead.club/@thomas/114676148254141233&lt;/a&gt; &lt;em&gt;(Den Post hätte ich wohl eher mit einer Umfrage-Option versehen sollen &amp;hellip; )&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Die letzte manuelle Zählung am 15.06.2025 hat ergeben:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Für 83 Benutzer (allerdings die meisten davon aus DE), hat die Zuteilung funktioniert&lt;/li&gt;
&lt;li&gt;Für 4 Benutzer hat diese nicht funktioniert. 3 davon wurden in die USA weitergeleitet statt Frankfurt - ein Benutzer wurde sogar nach Singapur verbunden, obwohl er in Deutschland war.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ich habe den Scaleway Support dazu befragt, welche GeoIP Datenbank genutzt wird und wie häufig diese aktualisiert wird. Folgende Antwort habe ich erhalten:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There can occasionally be discrepancies, such as the one you experienced with the Singapore data center being identified as European. It can take time for GeoIP databases to reflect changes like IP reallocation.
Our GeoIP database provider involve frequent updates, often on a weekly or bi-weekly basis.
While we don&amp;rsquo;t publicly disclose the specific third-party GeoIP database provider we use, we rely on a reputable industry-standard provider to ensure the highest possible accuracy for our customers.
We understand that accurate GeoIP routing is crucial for latency-sensitive applications. If you encounter significant and persistent inaccuracies, we encourage you to report them to our support team so we can investigate further.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Außerdem habe ich nachgehakt, ob bei Scaleway GeoIP nur die Resolver-Adresse für die Ortung genutzt wird, oder auch eine Auswertung des EDNS ECS Felds erfolgt. Das würde die Genauigkeit verbessern:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our product team has got back to us to indicate that you are right.
First we use the EDNS/ECS feature and if it’s not available we use resolver’s IP address.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="wie-gehts-weiter"&gt;Wie geht&amp;rsquo;s weiter?&lt;/h2&gt;
&lt;p&gt;Mit einem Anycast-basiertem CDN könnte man sicherlich deutlich bessere Ergebnisse erzielen, aber das ist für mich aus den oben genannten Gründen ja erst einmal keine implementierbare Lösung. Da mein kleines GeoIP CDN zwar nicht in allen, aber in den meisten Fällen eine Verbesserung erreicht, werde ich das Experiment weiterlaufen lassen. Vielleicht gibt es noch die ein oder andere Stellschraube, mit der man die Lokalisierung verbessern kann.&lt;/p&gt;
&lt;p&gt;Ansonsten spiele ich auch mit dem Gedanken, evtl. doch auf ein professionelleres, fremd gehostetes und AnyCast CDN umzusteigen. Dann wäre Cloudflare allerdings der einzige Anbieter, auf den ich mich stützen könnte. Denn das &amp;ldquo;100 % Green Energy&amp;rdquo; Label meiner Dienste bin ich nicht bereit für ein CDN aufzugeben.&lt;/p&gt;
&lt;p&gt;Insgesamt kann man sagen, dass mein GeoIP-CDN hilft, den Zugriff für die meisten weit entfernten Benutzer zu verbessern. Es gibt aber auch Einschränkungen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nicht in allen Fällen wird der passende CDN-Server zugewiesen (Ungenauigkeit der GeoIP Datenbank)&lt;/li&gt;
&lt;li&gt;Für einen Latenzvorteil muss ein bestimmter Inhalt bereits von einem anderen Benutzer aus der Region abgerufen worden sein (Inhalte werden (noch?) nicht über alle CDN-Server synchron vorgehalten =&amp;gt; Mehr Speicherbedarf)&lt;/li&gt;
&lt;li&gt;Nur Medieninhalte werden vom CDN vorgehalten. API-Services und Webfrontend werden nach wie vor nur in der EU gehostet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="weitere-verbesserungen-dns-optimierung"&gt;Weitere Verbesserungen: DNS-Optimierung&lt;/h2&gt;
&lt;p&gt;Nachdem ich mein CDN ausgerollt habe, habe ich durch Performancetools festgestellt, dass die DNS-Namensauflösung vor allem für weit entfernte Benutzer einen großen Teil der gesamten Ladezeit einnimmt. Wie sich das verbessern lässt, habe ich in diesem Folgeartikel beschrieben: &amp;ldquo;&lt;a href="https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/"&gt;Globale DNS-Auflösung durch Verzicht auf CNAMES beschleunigen&lt;/a&gt;&amp;rdquo;&lt;/p&gt;</description></item><item><title>Mastodon Media Storage: Woher kommt der Overhead im Minio S3 Speicher?</title><link>https://thomas-leister.de/mastodon-s3-minio-overhead/</link><pubDate>Sat, 07 Jun 2025 10:27:30 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/mastodon-s3-minio-overhead/</guid><description>&lt;p&gt;Gestern habe ich mir ein kleines Tool geschrieben, das ermittelt, wie die Dateigrößenverteilung in meinem metalhead.club S3 Storage ist.&lt;/p&gt;
&lt;p&gt;Wichtig: Das bezieht sich auf den Storage-Layer von meinem Minio-Server - nicht direkt auf die Dateien, wie sie hochgeladen wurden. Minio legt noch Metadateien dazu bzw. integriert kleinere Dateien in eigene Strukturen und legt diese dann im Dateisystem ab. Siehe: &lt;a href="https://blog.min.io/minio-optimizes-small-objects/"&gt;Working with Small Objects in AI/ML workloads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Ich wollte trotzdem mal sehen, wie viel Speicher durch Blocksize-Overhead verloren geht.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root@s3 /root # time ./filesize-analyzer /mnt/s3storage1/metalheadclub-media
Dateigrößenstatistik:
&amp;gt;= 1024K: 136138 Dateien
&amp;lt; 1K: 913982 Dateien
&amp;lt; 2K: 53357 Dateien
&amp;lt; 4K: 99581 Dateien
&amp;lt; 8K: 182476 Dateien
&amp;lt; 16K: 383095 Dateien
&amp;lt; 32K: 646094 Dateien
&amp;lt; 64K: 711153 Dateien
&amp;lt; 128K: 419150 Dateien
&amp;lt; 256K: 389334 Dateien
&amp;lt; 512K: 260465 Dateien
&amp;lt; 1024K: 134658 Dateien
Theoretische Gesamtgröße der Dateien kleiner als 4K: 766435063 Bytes
Tatsächliche Gesamtgröße der Dateien kleiner als 4K: 4370104320 Bytes
./filesize-analyzer /mnt/s3storage1/metalheadclub-media 100,41s user 1157,35s system 21% cpu 1:36:57,26 total
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Das Ergebnis: 1.066.920 Dateien sind kleiner als die Dateisystem-Blocksize von 4K (ext4). Diese brauchen also beim Ablegen im Dateisystem mehr Speicher, als sie eigentlich groß sind. Das kommt daher, dass für jede Datei - auch wenn sie kleiner ist - im Dateisystem mindestens 4 KB (Kilobyte, 4 x 1024 bytes) reserviert werden. All diese Dateien erzeugen also einen Overhead.&lt;/p&gt;
&lt;p&gt;Durch den Vergleich in den letzten beiden Zeilen wird klar, wie groß der Overhead ist: Eigentlich sind die betreffenden Dateien nur 766435063 Bytes (= &lt;strong&gt;731 MB&lt;/strong&gt;) groß, doch der Blocksize-Overhead im Dateisystem führt dazu, dass diese real 4370104320 Bytes (= &lt;strong&gt;4167 MB&lt;/strong&gt;) belegen. Das ist 570 % ihrer eigentlichen Größe.&lt;/p&gt;
&lt;p&gt;Übrigens ist das nicht die ganze Geschichte: Mein Tool berücksichtigt im Moment noch keine Bruchstücke von Dateien, die größer oder gleich 4K sind. Eigentlich kommt nämlich auch bei den größeren Dateien noch Overhead durch die Blocksize dazu. Nämlich dann, wenn ihre Größe nicht genau ein vielfaches von 4K ist. Im Fall der größeren Dateien spielt der Overhead im Verhältnis zur abgespeicherten Datenmenge allerdings kaum mehr eine Rolle, daher habe ich ihn bei der Auswertung ignoriert. &lt;strong&gt;Besonders dramatisch Fällt der Overhead bei den ganz kleinen Dateien aus&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Wie ihr an der letzten Zeile der Ausgabe erkennen könnt, hat die Auswertung des metalhead.club Media Storage knapp über 1,5 Stunden gedauert. Das ist nicht besonders schnell - allerdings liegen im Storage auch etwa 4,3 Millionen Dateien, die einzeln auf ihre Größe untersucht wurden - dazu noch verschachtelt in relativ tiefen Verzeichnishierarchien.&lt;/p&gt;
&lt;p&gt;Mein Tool zur Analyse ist bisher nur ein Proof of concept. Womöglich habe ich noch den ein oder anderen Bug in meinem Code. Die Berechnung sieht realistisch aus, doch ich hoffe, noch bessere Performance herauskitzeln zu können. Da gibt es sicherlich noch Verbesserungspotential. Daher habe ich es noch nicht veröffentlicht. Sollte ich aber einmal zufrieden damit sein, steht einer Veröffentlichung natürlich nichts im Wege. Interessanterweise konnte ich auf die Schnelle kein solches Tool finden, das einem eine kleine Statistik über die Dateigrößen in einem Verzeichnis liefert. Vielleicht habe ich auch nur nicht genau genug gesucht &amp;hellip;&lt;/p&gt;
&lt;p&gt;Auf das Thems bin ich übrigens gekommen, nachdem das Monitoring meines S3 Servers Alarm schlug, da nicht mehr besonders viel Speicher auf dem SSD-Speicher frei war. Ich war verwundert und habe (zu einem früheren Zeitpunkt) kurz die Größenangaben der verschiedenen Systeme verglichen.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mastodon sagt: 603 GiB Medien gespeichert&lt;/li&gt;
&lt;li&gt;Minio S3 sagt: 692 GiB Medien gespeichert&lt;/li&gt;
&lt;li&gt;Filesystem sagt: 763 GiB Medien gespeichert&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Da die Angaben zum Speicherverbrauch so stark abwichen, begab ich mich auf Spurensuche. Wie konnte es sein, dass Mastodon der Meinung war, dass es nur 603 GiB an Medien besaß, aber das Dateisystem mit 763 GiB deutlich mehr belegt war? Ich hoffte, darauf eine Antwort zu finden. Da Mastodon sehr viele sehr kleine Dateien abspeichert (Vorschaubilder, Avatare, &amp;hellip;), lag mein Verdacht auf dem Blocksize-Overhead. Wie ihr seht, ist dieser im Verhältnis zur Originaldateigröße beträchtlich. Aber nicht bedeutend genug, als dass er zumindest die 763-692 GiB = 71 GiB Unterschied zwischen Minio und Filesystem erklären könnte. Denn bezogen auf meinen konkreten Fall bedeutet das nur, dass ich nur ca 3.4 GB mehr belege, als theoretisch nötig - nicht 71 GiB.&lt;/p&gt;
&lt;p&gt;Und dann wäre da noch der Unterschied zwischen Mastodon und Minio. Der könnte evtl. an verwaisten Dateien liegen, die zwar noch im Minio S3 Speicher liegen, aber von denen Mastodon aus irgendeinem Grund nichts mehr weiß. Ich habe zwar ein &lt;code&gt;tootctl media remove-orphans&lt;/code&gt; ausgeführt, das den Speicher um solch verwaiste Dateien eigentlich bereinigen sollte. Doch dabei sind auch nur 2.8 GB Speicher frei geworden.&lt;/p&gt;
&lt;p&gt;Falls sich jemand tiefergehend in der Materie auskennt: Schreibt mich gerne an! Entweder auf Mastodon (&lt;code&gt;thomas@metalhead.club&lt;/code&gt;) oder per &lt;a href="https://thomas-leister.de/impressum/"&gt;E-Mail&lt;/a&gt;. Würde mich freuen, wenn ich mir die Unterschiede erklären könnte.&lt;/p&gt;</description></item><item><title>Faircamp als selbstgehostete Alternative zu Bandcamp</title><link>https://thomas-leister.de/bandcamp-alternative-faircamp/</link><pubDate>Thu, 29 May 2025 16:41:57 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/bandcamp-alternative-faircamp/</guid><description>&lt;p&gt;Meine Mastodon-Instanz &lt;a href="https://metalhead.club"&gt;metalhead.club&lt;/a&gt; hat mittlerweile schon einige Unterprojekte hervorgebracht: &lt;a href="https://thomas-leister.de/selling-t-shirts-for-mastodon-metalhead-club/"&gt;T-Shirts&lt;/a&gt;, &lt;a href="https://blog.650thz.de/posts/metalheadclub-patches-2024/"&gt;Patches&lt;/a&gt;, &lt;a href="https://blog.650thz.de/posts/metalheadclub-flyer/"&gt;Flyer&lt;/a&gt;, &lt;a href="https://blog.650thz.de/posts/metalheadclub-poster/"&gt;Plakate&lt;/a&gt;, Sticker und sogar ein &lt;a href="https://blog.650thz.de/posts/metalheadclub-bier/"&gt;Bier&lt;/a&gt;. Alles mit dem metalhead.club-Branding.&lt;/p&gt;
&lt;p&gt;Nun ist aber ein ganz besonderes Projekt fertig geworden, an dem ich nicht direkt beteiligt war: &lt;a href="https://music.metalhead.club/metalheadclub-anthem/"&gt;Ein metalhead.club Song&lt;/a&gt;! Oder wohl eher: Eine Hymne!&lt;/p&gt;
&lt;p&gt;Einige talentierte Member haben sich im Sommer 2024 zusammengeschlossen, um einen Song für und über ihre Lieblings-Mastodoninstanz zu schreiben - und das ist ihnen sehr gelungen! Das Ergebnis kann &lt;a href="https://music.metalhead.club/metalheadclub-anthem/"&gt;hier&lt;/a&gt; angehört und gegen eine kleine Spende auch heruntergeladen werden.&lt;/p&gt;
&lt;h2 id="bandcamp-als-künstler-nur-mit-paypal"&gt;Bandcamp: Als Künstler nur mit PayPal&lt;/h2&gt;
&lt;p&gt;Nun galt es allerdings, den Song auf einer passenden Plattform zu veröffentlichen und interessierten Membern und Nutzern anderer Mastodon-Instanzen zum Download anzubieten. Mein erster Gedanke ging Richtung Bandcamp. Dort höre und kaufe ich seit vielen Jahren sehr gerne Musik. Der separate Künstleraccount war schnell angelegt, doch relativ schnell weichte die anfängliche Begeisterung der Ernüchterung: Bandcamp unterstützt (zumindest hier in Deutschland) einzig und allein den Zahlungsdienstleister PayPal.&lt;/p&gt;
&lt;p&gt;Während man als normaler Bandcamp-Nutzer wenigstens noch auf die Zahlung via Kreditkarte ausweichen kann, bleibt bei Künstleraccounts nur die Verknüpfung mit einem PayPal Konto. Anders lassen sich Zahlungen für die entstandenen Einnahmen nicht entgegennehmen. Sehr schade, denn Sinn und Zweck der Veröffentlichung sollte (auch aus Sicht der Band!) sein, dass die beim Verkauf eingenommenen Gelder für die weitere Finanzierung der Mastodon-Instanz verwendet werden sollten. Da Bandcamp keinerlei Alternativen anbietet, war die Plattform für mich also eine Sackgasse. PayPal ist für mich seit dem &lt;a href="https://blog.650thz.de/posts/the-paypal-incident/"&gt;Zwischenfall im Dezember 2022&lt;/a&gt; ein No-Go!&lt;/p&gt;
&lt;h2 id="faircamp-als-alternative"&gt;Faircamp als Alternative?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://metalhead.club/@MiseryPath"&gt;@MiseryPath&lt;/a&gt;, der das Song-Projekt ins Leben gerufen hat, brachte dann aber relativ schnell das richtige Stichwort ins Spiel: &amp;ldquo;Faircamp&amp;rdquo;!&lt;/p&gt;
&lt;p&gt;Bei &lt;a href="https://simonrepp.com/faircamp/"&gt;Faircamp&lt;/a&gt; handelt es sich nicht etwa um eine Bandcamp-Kopie, sondern genau genommen nur um einen Static Site Generator, aus dem sich Künstler ihre eigenen kleinen Bandcamp-ähnlichen Websites generieren können. Das Projekt wurde von &lt;a href="https://post.lurk.org/@freebliss"&gt;Simon Repp&lt;/a&gt; ins Leben gerufen und maßgeblich entwickelt. Von Faircamp habe ich vor einer Weile schon einmal gehört und auch schon einzelne Künstlerseiten gesehen, die darauf basieren.&lt;/p&gt;
&lt;p&gt;Also habe ich mir das Tool selbst einmal genauer angesehen und nach ersten Gehversuchen damit war ich schnell begeistert! Faircamp eignet sich hervorragend für genau das, wofür es vorgesehen ist: In kurzer Zeit eine eigene Bandcamp-ähnliche Website an den Start zu kriegen! Und das geht so:&lt;/p&gt;
&lt;p&gt;Je nach Plattform wird das Faircamp Binary auf dem System installiert. Unter Fedora Linux ist das mit wenigen Kommandos erledigt:&lt;/p&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-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1&lt;/span&gt;&lt;span&gt;sudo dnf install cmake ffmpeg-free gcc git opus-devel rust vips-devel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2&lt;/span&gt;&lt;span&gt;git clone https://codeberg.org/simonrepp/Faircamp.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3&lt;/span&gt;&lt;span&gt;cd Faircamp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4&lt;/span&gt;&lt;span&gt;cargo install --features libvips --locked --path .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Danach wird der sogenannte &amp;ldquo;Catalog&amp;rdquo; angelegt: Ein Verzeichnis, das die zu veröffentlichen Musikdateien und (je nach Anpassungsbedarf) auch ein paar Konfigurationsdateien enthält. Hat man seine Verzeichnisstruktur fertiggestellt und alle Musikdateien passend sortiert abgelegt, wird in diesem Verzeichnis nur noch das &lt;code&gt;faircamp&lt;/code&gt; Executable aufgerufen.&lt;/p&gt;
&lt;p&gt;Und fertig ist die Website. Das Executable nimmt sich alle Dateien, konvertiert die Songs in die vorgegebenen bzw. passende Dateiformate, bringt auf Wunsch Metadaten mit ein und erzeugt alle HTML-Dateien und Assets, die für den Betrieb einer kleinen Website erforderlich sind. Die Dateien können dann per FTP oder SSH auf einen beliebigen Webspace hochgeladen werden. Dieser muss nur das Minimum von dem Erfüllen, was ein Webspace erfüllen muss: Er muss Dateien ausliefern können. Datenbank, PHP, Python oder irgendwelche speziellen Einstellungen werden nicht benötigt - der Charme eines Static Site Generators eben &amp;hellip;&lt;/p&gt;
&lt;p&gt;Das Resultat kann dann beispielsweise so aussehen:&lt;/p&gt;
&lt;figure class="full-width"&gt;&lt;img src="https://thomas-leister.de/bandcamp-alternative-faircamp/images/faircamp.png"
alt="Screenshot von Faircamp unter music.metalhead.club"&gt;&lt;figcaption&gt;
&lt;p&gt;Screenshot von Faircamp unter music.metalhead.club&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Das Basis-Design ist vorgegeben. Allerdings lassen sich je nach Geschmack auch einige Schaltflächen oder Sektionen ein- oder ausblenden, umbenennen und die Farben für Hintergrund und Akzentfarben anpassen. Ich habe mich für ein ziemlich schlichtes Design entschieden.&lt;/p&gt;
&lt;h2 id="softes-abkassieren"&gt;&amp;ldquo;Softes&amp;rdquo; Abkassieren&lt;/h2&gt;
&lt;p&gt;Mit dabei ist auch eine &amp;ldquo;Weiche Bezahlschranke&amp;rdquo; (&amp;ldquo;Soft paycurtain&amp;rdquo;). Soll heißen: Man kann für die angebotenen Musikdateien durchaus einen Preis angeben. Entweder einen Fixpreis oder einen Preisbereich. Der Nutzer kann dann im Rahmen dessen selbst entscheiden, wie viel er für einen Download zu zahlen bereit ist. Allerdings wird der Bezahlvorgang bei dieser &amp;ldquo;soft&amp;rdquo; Variante nicht verifiziert, denn ob der Nutzer für einen Download gezahlt hat (oder nicht!), können wir technisch nicht abfragen. Das liegt in der Natur einer statischen Website: Anders als bei anderen Modellen kann der Server hier im Hintergrund keine Logik verarbeiten und somit nicht überprüfen, ob tatsächlich eine Zahlung getätigt wurde. Auch eine clientseitige Verifizierung ist relativ sinnlos - diese lässt sich sehr einfach manipulieren.&lt;/p&gt;
&lt;p&gt;Insofern ist man spätestens hier auf die Ehrlichkeit der Nutzer angewiesen: Wer an dem Track interessiert ist, wird mit der entsprechenden &amp;ldquo;Ich habe bereits gezahlt&amp;rdquo;-Schaltfläche hoffentlich zurecht quittieren, dass er oder sie zu dem Download berechtigt ist. Selbstverständlich lässt sich die Bezahlung kinderleicht umgehen, wenn man das will.&lt;/p&gt;
&lt;p&gt;Das lässt sich aus meiner Sicht in diesem Fall aber absolut verkraften. Denn mit der Musik soll nicht Geld gescheffelt werden - es geht darum, potentiellen metalhead.club-Spendern für ihre Spende eine kleine Belohnung in Form des Song-Downloads zu geben. Und das kann mit Faircamp ganz wunderbar umgesetzt werden - und ganz ohne PayPal, ohne Gebühren für Zahlungsdienstleister und ohne Aufschläge in Form von Steuern, weil der Plattformanbieter beispielsweise in den USA sitzt &amp;hellip; ;-)&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Wer auch mit dem Gedanken spielt, seine Musik auf einer selbst gehosteten Website zum Kauf oder kostenlosen Download anzubieten, sollte sich Faircamp einmal ansehen. Zumindest dann, wenn eine Bezahlung nicht verifiziert werden muss und es keinen Nutzeraccount mit eigener Bibliothek braucht, kann Faircamp eine tolle Alternative zu den zentralen Plattformen wie Soundcloud oder Bandcamp sein.&lt;/p&gt;</description></item><item><title>Ein Windows 10 USB Bootmedium unter Fedora / Linux erstellen</title><link>https://thomas-leister.de/creating-a-windows-10-boot-medium-from-fedora-linux/</link><pubDate>Wed, 02 Oct 2024 11:52:12 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/creating-a-windows-10-boot-medium-from-fedora-linux/</guid><description>&lt;p&gt;Kürzlich habe ich einen gebrauchten und betagten Acer Aspire E774 Laptop als Spende für den &lt;a href="https://computertruhe.de/spenden/sachspenden/"&gt;Computertruhe e.V.&lt;/a&gt; fertiggemacht. Selbstverständlich gehört dazu auch das überschreiben der Festplatten mit zufälligen Daten, sodass sich keine alten Daten mehr rekonstruieren lassen. Denn &amp;ldquo;gelöscht&amp;rdquo; ist nicht gleich &amp;ldquo;sicher gelöscht&amp;rdquo;. Zum sicheren Löschen habe ich das Tool &lt;code&gt;shred&lt;/code&gt; in einer Fedora Live Umgebung genutzt. Mit beliebigen anderen Linux-Distributionen funktioniert das aber genauso gut.&lt;/p&gt;
&lt;p&gt;Da ein Ubuntu auf dem Laptop nicht auf Anhieb lauffähig bzw. zumutbar war, weil das Touchpad nicht funktionierte und es beim Boot immer wieder merkwürdige Probleme gab, habe ich mich dazu entschlossen, nach meiner Löschaktion einfach wieder ein Windows 10 zu installieren. Ganz so, wie es zuletzt auf dem Laptop einwandfrei lief.&lt;/p&gt;
&lt;p&gt;Kurzerhand habe ich also bei Microsoft ein WIndows 10 64-Bit Image heruntergeladen und wollte dieses auf meinen USB-Stick kopieren:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dd if=~/Downloads/Win10.img of=/dev/sdc bs=8M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nachdem das Image gebrannt war, habe ich den Stick am Laptop eingesteckt und über eine der F-Tasten beim Start den Bootmanager aufgerufen. Üblicherweise lässt sich das Bootmedium vor dem OS-start hier noch einmal anpassen, wenn die UEFI-Einstellung nicht schon korrekt ist. Schließlich wollte ich ja vom USB-Stick starten und nicht von einer der beiden verbauten Festplatten.&lt;/p&gt;
&lt;p&gt;Doch: Nichts! Für den USB-Stick gab es keinen Eintrag. Ein Blick ins BIOS offenbarte, dass ein Start von einem USB-Medium zwar nicht die höchste Priorität hatte, aber selbst, nachdem ich das geändert hatte, zeigte mir das BIOS bzw. UEFI nur &amp;ldquo;No Boot Medium found&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Na gut. Vielleicht war mein Bootmedium ja defekt. Ich versuchte es mit dem Fedora Media Writer - einem grafischen Tool, das standardmäßig in jeder Fedora-Installation enthalten ist. Über den Media Writer können Fedora Images auf USB-Sticks kopiert werden, aber auch beliebige andere Betriebssystemimages. Aber auch über den Fedora Media Writer gelangte ich zu keinem bootbaren USB-Stick: Selber Fehler. Auch das umstecken in andere USB-Slots brachte keinen Erfolg. Zuletzt versuchte ich sogar die manuelle Formatierung des USB-Sticks und ein anschließendes manuelles Kopieren der Dateien. Ich habe mich dabei nach dieser Anleitung gerichtet: &lt;a href="https://akolles.de/sonstiges/windows-10-bootstick-erstellen"&gt;&amp;ldquo;Windows 10 Bootstick erstellen&amp;rdquo;&lt;/a&gt;. Doch wieder kein Erfolg.&lt;/p&gt;
&lt;p&gt;Schließlich kam ich über Umwege auf das Tool &lt;code&gt;woeusb&lt;/code&gt;. Mir gefiel, dass es aus den Fedora Paketquellen installierbar war und - im Gegensatz zu Unetbootin und anderen Alternativen - über die Kommandozeile lief. Also auch in meiner Wayland-Umgebung (anders als Unetbootin!).&lt;/p&gt;
&lt;p&gt;Mein Windows 10 Image habe ich also wie folgt auf meinen USB-Stick kopiert:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo woeusb --device ~/Downloads/Win10_22H2_German_x64v1.iso /dev/sda
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; und hatte damit Erfolg!&lt;/p&gt;
&lt;p&gt;Nun - wieso hat das funktioniert und meine vorherigen Versuche nicht? Das Log von &lt;code&gt;woeusb&lt;/code&gt; liefert eine Erklärung:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WoeUSB v5.2.4
==============================
Info: Mounting source filesystem...
Info: Wiping all existing partition table and filesystem signatures in /dev/sda...
/dev/sda: 8 bytes were erased at offset 0x00000200 (gpt): 45 46 49 20 50 41 52 54
/dev/sda: 8 bytes were erased at offset 0x734ffffe00 (gpt): 45 46 49 20 50 41 52 54
/dev/sda: 2 bytes were erased at offset 0x000001fe (PMBR): 55 aa
/dev/sda: calling ioctl to re-read partition table: Erfolg
Info: Ensure that /dev/sda is really wiped...
Info: Creating new partition table on /dev/sda...
Info: Creating target partition...
Info: Making system realize that partition table has changed...
Info: Wait 3 seconds for block device nodes to populate...
mkfs.fat 4.2 (2021-01-31)
mkfs.fat: Warning: lowercase labels might not work properly on some systems
Info: Mounting target filesystem...
Info: Copying files from source media...
Splitting WIM: 4900 MiB of 4900 MiB (100%) written, part 2 of 26%
Finished splitting &amp;quot;./sources/install.wim&amp;quot;
Info: Installing GRUB bootloader for legacy PC booting support...
i386-pc wird für Ihre Plattform installiert.
installation beendet. Keine Fehler aufgetreten.
Info: Installing custom GRUB config for legacy PC booting...
Info: Done :)
Info: The target device should be bootable now
Info: Unmounting and removing &amp;quot;/tmp/woeusb-source-20240930-085645-Sunday.0twgOL&amp;quot;...
Info: Unmounting and removing &amp;quot;/tmp/woeusb-target-20240930-085645-Sunday.2DBYAG&amp;quot;...
Info: You may now safely detach the target device
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interessant sind hier zwei Stellen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zum einen wird das USB-Stick offenbar mit FAT formatiert (mkfs.fat) und nicht - wie in anderen Anleitungen zu lesen - mit NTFS&lt;/li&gt;
&lt;li&gt;Und: &lt;code&gt;install.wim&lt;/code&gt; wird &amp;ldquo;gesplittet&amp;rdquo;?!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Der Split der &lt;code&gt;install.wim&lt;/code&gt; Datei erklärt, wieso es mit den anderen Methoden bei mir nicht funktioniert hat: Mit diesem Schritt wird die große Imagedatei (&amp;gt; 4 GB) auf kleinere Dateien aufgeteilt. Das ist nötig, weil das FAT Dateisystem nur mit Dateien bis zu einer Größe von 4 GB umgehen kann.&lt;/p&gt;
&lt;p&gt;Und warum liefert Microsoft in seinem Image, welches ich per &lt;code&gt;dd&lt;/code&gt; zu kopieren versucht habe, eine so große Datei aus? Weil das Microsoft Windows 10 ISO NTFS-formatiert ist. Und nicht FAT-formatiert. NTFS kann mit so großen Dateien problemlos umgehen - daher wurde mir in diversen Anleitungen auch dazu geraten, den USB-Stick nicht mit FAT, sondern mit NTFS zu formatieren.&lt;/p&gt;
&lt;p&gt;Allerdings kommt hier eine weitere Besonderheit zum Tragen: Das Acer Laptop unterstützt offenbar keine NTFS-Bootmedien. &amp;hellip; Wie das sein kann?&lt;/p&gt;
&lt;p&gt;Der Laptop kam ursprünglich mit Windows 8 auf dem Markt. Damals enthielt das Windows Bootmedium noch keine Dateien, die größer als 4 GB waren. Dementsprechend kam man mit dem FAT Dateisystem aus. Folglich unterstützt der Acer E774 Laptop nur FAT im UEFI und nicht NTFS. Das erklärt auch, wieso er problemlos von allen Live-Linux USB-Sticks bootete, die ich ihm gab. Hier wird noch FAT benutzt.&lt;/p&gt;
&lt;p&gt;Zusammengefasst: mit dem &lt;code&gt;woeusb&lt;/code&gt; Tool hatte ich letztendlich Erfolg, weil:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Der Dell-Laptop nur FAT Medien unterstützt&lt;/li&gt;
&lt;li&gt;Das woeusb Tool FAT nutzt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Und dabei aber dafür sorgt, dass keine der Dateien größer als 4 GB ist!&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Der Schlüssel ist also &lt;code&gt;woeusb&lt;/code&gt;&amp;rsquo;s &amp;ldquo;split&amp;rdquo; Funktion.&lt;/p&gt;
&lt;p&gt;Auf neueren Laptops sollte das Kopieren des Win10 Images auch einfach via &lt;code&gt;dd&lt;/code&gt; funktionieren. Für meinen älteren Laptop habe ich mit &lt;code&gt;woeusb&lt;/code&gt; aber eine tolle Lösung gefunden.&lt;/p&gt;</description></item></channel></rss>