<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>thomas-leister.de</title><link>https://thomas-leister.de/en/</link><description>Linux, server and Open Source weblog of Thomas Leister</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>thomas.leister@mailbox.org (Thomas Leister)</managingEditor><webMaster>thomas.leister@mailbox.org (Thomas Leister)</webMaster><lastBuildDate>Fri, 23 Jan 2026 15:03:21 +0100</lastBuildDate><atom:link href="https://thomas-leister.de/en/" rel="self" type="application/rss+xml"/><item><title>Migrate Mastodon Media Storage from Minio S3 to SeaweedFS S3</title><link>https://thomas-leister.de/en/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/en/mastodon-switch-minio-s3-seaweedfs/</guid><description>&lt;p&gt;Until now, I have enjoyed using the &lt;a href="https://www.min.io/"&gt;Minio S3 Server&lt;/a&gt; together with my &lt;a href="https://metalhead.club"&gt;Mastodon instance metalhead.club&lt;/a&gt; to store media files. However, since last summer, the Minio project has unfortunately taken a turn that has prompted me to look for alternatives: First, the internal file browser was removed from the administration interface. Shortly thereafter, it was announced that Minio would no longer be maintained as an open source project. In addition, the distribution of Minio binaries was discontinued. This meant that there were no more updates. Minio has effectively been dead ever since at least as far as the community version is concerned.&lt;/p&gt;
&lt;p&gt;However, I found a great alternative in &lt;a href="https://github.com/seaweedfs/seaweedfs"&gt;SeaweedFS&lt;/a&gt;, which I recently switched my Mastodon instance to. Here, I describe the problems I encountered and how I solved them.&lt;/p&gt;
&lt;h2 id="install-seaweedfs"&gt;Install SeaweedFS&lt;/h2&gt;
&lt;p&gt;Installation is very simple: SeaweedFS consists of a single binary, which is downloaded and stored in &lt;code&gt;/usr/local/bin&lt;/code&gt;:&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;Next, a new SeaweedFS system user is created:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;useradd --system seaweedfs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;and a corresponding Systemd service is created:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/etc/systemd/system/seaweedfs.service&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; starts the SeaweedFS binary &lt;code&gt;weed&lt;/code&gt; and with it the components &lt;code&gt;master server&lt;/code&gt;, &lt;code&gt;volume server&lt;/code&gt;, &lt;code&gt;filer&lt;/code&gt;, and the &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;: Internal communication interfaces and the S3 API should remain internal to &lt;code&gt;localhost&lt;/code&gt;, as we are not running SeaweedFS in a cluster and the S3 interface should only be accessible to the internal Nginx proxy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-volume.max=0&lt;/code&gt; ensures that the number of storage volumes is not limited. Otherwise, you could set the maximum amount of storage SeaweedFS is allowed to use (default: 1 volume = 30 GB). In my case, I want to use all available storage.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-dir /mnt/s3storage1/seaweedfs&lt;/code&gt;: This is where the volumes are stored in the form of files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Activate and start the service:&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;SeaweedFS should start after a few seconds. See also:&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="create-a-bucket"&gt;Create a bucket&lt;/h2&gt;
&lt;p&gt;A new S3 bucket can now be created and configured using the “weed shell.” In my case, the new bucket is called “metalheadclub-media.”&lt;/p&gt;
&lt;p&gt;Open shell:&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;Create 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-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;Set permissions (dynamically, instead of a json config file):&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;The following is displayed for confirmation:&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;Anonymous (public) users only have read access to the media 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 -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;Output:&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;You can then exit the shell by typing “quit.”&lt;/p&gt;
&lt;h2 id="migrating-data-from-minio-to-seaweedfs"&gt;Migrating data from Minio to SeaweedFS&lt;/h2&gt;
&lt;p&gt;The SeaweedFS server is now active, the bucket has been created and assigned the necessary permissions. Time to migrate the data from Minio to SeaweedFS!&lt;/p&gt;
&lt;p&gt;There were two things to keep in mind for the migration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The migration should be as “silent” as possible and have no impact on the users of my Mastodon instance.&lt;/li&gt;
&lt;li&gt;Since I wanted to use the same virtual machine with the same storage for SeaweedFS and storage was limited, the data should be moved from Minio to SeaweedFS, not copied!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As with previous migrations, the strategy was as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Put the new backend into operation&lt;/li&gt;
&lt;li&gt;Configure the frontend proxy with both backends =&amp;gt; Frontend first queries the new backend, falling back to the old backend in case of error&lt;/li&gt;
&lt;li&gt;Reconfigure the Mastodon instance: Stores new media in the new backend, while old media remains accessible from the old backend&lt;/li&gt;
&lt;li&gt;Media is slowly moved from the old storage to the new storage in the background&lt;/li&gt;
&lt;li&gt;&amp;hellip; until the old storage is finally empty and can be removed&lt;/li&gt;
&lt;li&gt;Done!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Two S3 backends for a smooth transition&lt;/p&gt;
&lt;p&gt;I have visualized the configuration for data migration here:&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="The graphic shows the dependencies between the Nginx proxies and the S3 servers. It starts with a media.metalhead.club server, which forms the front end. Behind it, one path leads directly to Minio, while another path leads to the SeaweedFS backend via another proxy, s3.650thz.de."&gt;&lt;figcaption&gt;
&lt;p&gt;The graphic shows the dependencies between the Nginx proxies and the S3 servers. It starts with a media.metalhead.club server, which forms the front end. Behind it, one path leads directly to Minio, while another path leads to the SeaweedFS backend via another proxy, s3.650thz.de.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The upper row (media.metalhead.club, Minio server) already existed – the SeaweedFS backend and the s3.650thz.de server are new additions. The media.metalhead.club Nginx proxy was to play a special role again (similar to the &lt;a href="https://thomas-leister.de/en/switching-mastodon-from-scaleway-to-selfhosted-minio-s3/"&gt;migration from Scaleway S3 to Minio&lt;/a&gt;) and switch flexibly between the two S3 backends during the migration of the data. The new SeaweedFS backend should primarily respond to requests. In the event that the new backend was unable to deliver the data, a fallback was implemented to ensure that the request could then be answered by the old Minio backend.&lt;/p&gt;
&lt;p&gt;The key lies in these lines:&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-for-seaweedfs-s3"&gt;Virtual Host style addressing for SeaweedFS S3&lt;/h3&gt;
&lt;p&gt;An important difference in the handling of the SeaweedFS backend compared to Minio is that SeaweedFS does not support virtual host addressing of S3 buckets! With Minio, a bucket can be addressed in two ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bucket.s3server.tld&lt;/code&gt; or&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s3server.tld/bucket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The former is therefore not possible with SeaweedFS. Instead, the SeaweedFS Wiki suggests rewriting addresses in the latter form:&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;“”)&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;However, I was unable to implement this type of configuration in my media.metalhead.club VirtualHost because it led to an error:&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;So I quickly moved this mechanism to another vHost called &amp;ldquo;s3.650thz.de&amp;rdquo;:&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;Ultimately, the vHost only serves to convert “virtual host style addressing” into conventional “path style addressing.”&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(The “vHost style addressing” in the media.metalhead.club vHost requires the &lt;code&gt;proxy_pass&lt;/code&gt; directives, as paths such as &lt;code&gt;proxy_pass http://seaweedfs/bucket&lt;/code&gt; cannot be specified here.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My media.metalhead.club for the migration phase now looks like this:&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;Using &lt;code&gt;add_header X-Served-By “s3.650thz.de ...”&lt;/code&gt;, I also have the HTTP header indicate whether a file was delivered by Minio or SeaweedFS. This allows me to check later whether the data migration worked as hoped.&lt;/p&gt;
&lt;h3 id="configuring-the-mastodon-instance"&gt;Configuring the Mastodon instance&lt;/h3&gt;
&lt;p&gt;In order for Mastodon to store the new media files in SeaweedFS, the correct parameters had to be set in the &lt;code&gt;.env.production&lt;/code&gt; configuration:&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;After restarting all Mastodon services, the change was active:&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;I could easily check whether my change was successful: I waited for new posts with images to appear in the global Mastodon timeline and opened the image in a new tab. The Firefox developer tools then allow you to view the HTTP headers for the file. As hoped, the &lt;code&gt;X-Served-By&lt;/code&gt; header was set to “SeaweedFS.” :) &amp;hellip;&lt;/p&gt;
&lt;p&gt;and the old graphics could still be accessed. They were delivered with &lt;code&gt;X-Served-By&lt;/code&gt; “Minio.”&lt;/p&gt;
&lt;h3 id="data-transfer"&gt;Data transfer&lt;/h3&gt;
&lt;p&gt;Great! Now it was time to start the actual data migration. Away from Minio – over to SeaweedFS.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Actually&lt;/em&gt;, such a migration is not rocket science. I configured &lt;code&gt;rclone&lt;/code&gt; with both S3 buckets:&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;I tested access to both buckets with a simple file listing:&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;I then began testing by first copying only smaller directories such as &lt;code&gt;site_uploads&lt;/code&gt; from Minio to SeaweedFS (instead of moving them!):&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;Once again, I used browser tools to test how the affected graphics (e.g., banner graphics at &lt;a href="https://metalhead.club/about"&gt;https://metalhead.club/about&lt;/a&gt;) were delivered. While they were initially delivered by Minio, they were delivered by SeaweedFS after the &lt;code&gt;rclone sync&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After this test was also successful, I migrated the Mastodon media data piece by piece to SeaweedFS (this time by moving it with &lt;code&gt;move&lt;/code&gt; instead of copying it with &lt;code&gt;sync&lt;/code&gt;!).&lt;/p&gt;
&lt;p&gt;List of directories to be moved:&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;My commands for moving:&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;If you pay close attention, you will notice that I have omitted one directory: the media cache directory &lt;code&gt;/cache&lt;/code&gt;! This is by far the largest and receives special treatment &amp;hellip;&lt;/p&gt;
&lt;h3 id="trouble-with-the-large-cache-directory"&gt;Trouble with the large /cache directory&lt;/h3&gt;
&lt;p&gt;This directory contains all files that do not originate from your own Mastodon server. In other words, cached remote media data and preview images. Depending on the cache retention time, the directory can become very large. In the case of metalhead.club, I was dealing with about 700 GB and approximately 3 million small files. In fact, it&amp;rsquo;s not the total size that&amp;rsquo;s the problem, but the number of files.&lt;/p&gt;
&lt;p&gt;Of course, I also tried to move this directory to the new storage:&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; but after a few minutes, it failed due to insufficient RAM. I added more RAM (about 16 GB), but again, the &lt;code&gt;rclone&lt;/code&gt; tool was so RAM-hungry that it was terminated by the OOM killer.&lt;/p&gt;
&lt;p&gt;All right. If I couldn&amp;rsquo;t move all the data from the cache directory, I could just leave it in the old Minio bucket until it had accumulated! This is because the &lt;code&gt;/cache&lt;/code&gt; directory only contains files that are not very important for operation and are normally cleaned up by Mastodon after a specified period of time anyway (at metalhead.club: cache retention 15 days).&lt;/p&gt;
&lt;p&gt;“Just leave it and wait” sounds like an appealing strategy, but it&amp;rsquo;s not that simple. This is because the switch of the Mastodon software to the new bucket means that Mastodon&amp;rsquo;s internal deletion routines no longer work. The bucket remains as full as it was last used. And new files are pushing their way onto the system. The result: memory consumption increases.&lt;/p&gt;
&lt;p&gt;If you have a lot of free memory, you may actually be able to simply wait until 15 days have passed and then delete the entire bucket. However, I only had about 170 GB of free storage. This would have been used up well before 15 days by new media added to the new bucket. So waiting was not an option. The old media had to be deleted bit by bit.&lt;/p&gt;
&lt;p&gt;First, I tried using rclone commands. This command deletes all files older than 15 days:&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; - actually. Once again, the RAM requirements were too high (in some cases, 15 GB consumption by &lt;code&gt;rclone&lt;/code&gt; alone!).&lt;/p&gt;
&lt;p&gt;With a few optimizations, you can significantly reduce consumption:&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;See also: &lt;a href="https://rclone.org/faq/"&gt;https://rclone.org/faq/#&lt;/a&gt; rclone-is-using-too-much-memory-or-appears-to-have-a-memory-leak&lt;/p&gt;
&lt;p&gt;Just when I thought I had solved the problem, another error popped up:&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 “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”: net/http: timeout awaiting response headers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apparently, listing the files took so long that the S3 server itself timed out (after about 10 minutes). How should I solve this? There were simply too many files in the bucket.&lt;/p&gt;
&lt;p&gt;I also had no luck with Minio&amp;rsquo;s own tool &lt;code&gt;mc&lt;/code&gt;:&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;How could I get rid of 3 million small files? As it turned out, it was actually very simple&amp;hellip;&lt;/p&gt;
&lt;p&gt;Deleting files using S3 Lifecycle Rules&amp;hellip;&lt;/p&gt;
&lt;p&gt;because I had completely forgotten about a practical S3 mechanism: Lifecycle Rules. These rules allow you to specify what should happen to files during or after their lifecycle. One use case is automatic deletion. Exactly what I needed! And the best part is that this deletion takes place internally in the backend and does not have to be controlled by external tools. This allowed me to cleverly circumvent my timeout problem with file listing.&lt;/p&gt;
&lt;p&gt;So I used the &lt;code&gt;mc&lt;/code&gt; tool to expire all files in &lt;code&gt;/cache&lt;/code&gt; after 15 days:&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;Done!&lt;/p&gt;
&lt;p&gt;The current status can be viewed with&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;To make the Minio internal file scanner work faster and thus delete files more quickly when they have exceeded their expiration date, the process can be accelerated a little further:&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="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;After 15 days, all old files had been deleted and I was able to delete the Minio bucket, remove Minio, and also remove the old bucket from my Nginx configurations. The lines&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;and&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;were removed and Nginx was reloaded. Metalhead.club was now completely migrated to SeaweedFS.&lt;/p&gt;
&lt;p&gt;By the way, thanks to Stefano Marinelli (&lt;a href="https://mastodon.bsd.cafe/@stefano"&gt;@stefano@bsd.cafe&lt;/a&gt;) for the &lt;a href="https://it-notes.dragas.net/2025/11/06/self-hosting-your-mastodon-media-with-seaweedfs/"&gt;SeaweedFS article under FreeBSD&lt;/a&gt;. This ultimately convinced me to give SeaweedFS a try and tackle the migration.&lt;/p&gt;</description></item><item><title>Migrating old Dovecot configuration to Dovecot 2.4 (Debian Trixie)</title><link>https://thomas-leister.de/en/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/en/mailserver-migrate-config-to-dovecot-2.4-debian-trixie/</guid><description>&lt;p&gt;The new Debian version &amp;ldquo;Trixie&amp;rdquo; has been available for upgrading from Debian &amp;ldquo;Bookworm&amp;rdquo; for several weeks now – and as a result, some of my readers have decided to update their mail server setup from my &lt;a href="https://thomas-leister.de/mailserver-debian-buster/"&gt;&amp;ldquo;Mail server with Dovecot, Postfix, MySQL, and Rspamd on Debian 10 Buster [v1.0] (German)&amp;rdquo;&lt;/a&gt;. The setup described for Debian Buster works just as well for Debian Bookworm – but Debian Trixie brings a change:&lt;/p&gt;
&lt;div class="warning"&gt;
The latest Debian version makes a leap to Dovecot 2.4 with the included Dovecot version. This means that old Dovecot configurations are no longer compatible!
&lt;/div&gt;
&lt;p&gt;The configuration syntax has changed significantly in some respects, and old configurations from previous Dovecot versions can no longer be read. This article discusses the changes in the new version 2.4 and explains the migration using the example of the mail server instructions mentioned above. All changes take place in the file &lt;code&gt;/etc/dovecot/dovecot.conf&lt;/code&gt;. No further changes (e.g., to the database schema or similar) are necessary.&lt;/p&gt;
&lt;div class="tip"&gt;
You can find a complete configuration file at the end of this post.
&lt;/div&gt;
&lt;p&gt;Overall, the following has changed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The way configuration parameters can be nested&lt;/li&gt;
&lt;li&gt;Names of individual parameters&lt;/li&gt;
&lt;li&gt;Variable names and functions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All changes are described in the &lt;a href="https://doc.dovecot.org/main/installation/upgrade/2.3-to-2.4.html"&gt;Dovecot documentation&lt;/a&gt;. However, if you followed my instructions when setting up your mail server, you can simply continue reading and follow the steps described to achieve a working configuration.&lt;/p&gt;
&lt;h2 id="step-1-add-config-version-numbers"&gt;Step 1: Add config version numbers&lt;/h2&gt;
&lt;p&gt;Dovecot 2.4 introduces versioning of the configuration syntax. Therefore, the following lines must be added &lt;strong&gt;at the beginning of the file&lt;/strong&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
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;It seems that the omissions of the past have now been rectified ;-). Future changes to the syntax can then be intercepted by Dovecot itself or warnings can be issued in the event of changes.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="ssl-settings"&gt;SSL settings&lt;/h2&gt;
&lt;p&gt;The SSL settings are renamed or changed as follows:&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; (and omit the angle bracket at the beginning)&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; (and omit the angle bracket at the beginning)&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; (and omit the angle bracket at the beginning)&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;. Values should not be “yes” or “no”, but ‘client’ or “server”. Default: Client. Setting can be removed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssl_min_protocol&lt;/code&gt;: can be omitted, default is already 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;. Is default - can be omitted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Overall, this results in:&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-and-auth_username_format"&gt;PassDB / UserDB and auth_username_format&lt;/h2&gt;
&lt;p&gt;The two PassDB and UserDB sections become:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;passdb sql {
query = SELECT username AS user, domain, password FROM accounts WHERE username = ‘%{user | username | lower}’ AND domain = ‘%{user | domain | lower}’ and enabled = true;
default_password_scheme = SHA512-CRYPT
}
userdb sql {
query = SELECT concat(quota, ‘M’) AS quota_storage_size FROM accounts WHERE username = ‘%{user | username | lower}’ AND domain = ‘%{user | domain | lower}’ AND sendonly = false;
iterate_query = SELECT username, domain FROM accounts where sendonly = false;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Pay special attention to the customized variables, e.g., &lt;code&gt;%{user | username | lower }&lt;/code&gt;. Behind &lt;code&gt;concat(quota, ‘M’) AS quota_storage_size&lt;/code&gt; there is also a small bug fix that I want to include here. ;-)&lt;/p&gt;
&lt;p&gt;In addition, the variable is also customized for &lt;code&gt;auth_username_format&lt;/code&gt;:&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;In order for the UserDB and PassDB sections just defined to work at all, the access data for the MySQL database is defined in a new SQL section:&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; must of course be customized!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The file &lt;code&gt;/etc/dovecot/dovecot-sql.conf&lt;/code&gt; can be deleted completely—it is no longer needed.&lt;/p&gt;
&lt;h2 id="mail-location"&gt;Mail location&lt;/h2&gt;
&lt;p&gt;The definition of the mail storage location &lt;code&gt;mail_location&lt;/code&gt; is split into several parameters and replaced as follows:&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;In addition, the &lt;code&gt;mail_home&lt;/code&gt; parameter is given new variable names:&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;The &lt;code&gt;protocols&lt;/code&gt; setting and the two sections &lt;code&gt;protocol imap&lt;/code&gt; and &lt;code&gt;protocol lmtp&lt;/code&gt; become this 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;The &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;no longer exists. Instead, the content becomes global.&lt;/p&gt;
&lt;h3 id="sieve-plugin-settings"&gt;Sieve Plugin Settings&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;, and &lt;code&gt;sieve&lt;/code&gt; become:&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;All &lt;code&gt;imapsieve*&lt;/code&gt; parameters become:&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; becomes:&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; and &lt;code&gt;quota_exceeded_message&lt;/code&gt; become:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;quota “User quota” {
driver = count
}
quota_exceeded_message = User %{user} has exceeded the storage volume. / User %{user} has exhausted allowed storage space.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The old “fs” driver for quota should no longer be used. Instead, &lt;code&gt;count&lt;/code&gt; is now used. &lt;code&gt;quota_exceeded_message&lt;/code&gt; also gets new variable names.&lt;/p&gt;
&lt;p&gt;The quota plugin is activated in the new &lt;code&gt;mail_plugins&lt;/code&gt; blog:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mail_plugins {
quota = yes
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;imap_quota&lt;/code&gt; has also been activated in the previously mentioned &lt;code&gt;protocol imap&lt;/code&gt; block.&lt;/p&gt;
&lt;h2 id="changing-access-pemmissions"&gt;Changing access pemmissions&lt;/h2&gt;
&lt;p&gt;Since the MySQL access data is now stored directly in the main configuration file and is no longer stored in a separate file, access rights to &lt;code&gt;dovecot.conf&lt;/code&gt; are further restricted. No one except root should be able to read the file:&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="complete-sample-configuration"&gt;Complete sample configuration&lt;/h2&gt;
&lt;p&gt;For easier comparison, here is the complete file &lt;code&gt;/etc/dovecot/dovecot.conf&lt;/code&gt; again:&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: Added &lt;code&gt;default_password_scheme&lt;/code&gt; to &lt;code&gt;passdb&lt;/code&gt; section. Added &amp;ldquo;Changing access permissions&amp;rdquo; section.&lt;/li&gt;
&lt;li&gt;2025-11-26: Removed &lt;code&gt;user = vmail&lt;/code&gt; from &lt;code&gt;service lmtp&lt;/code&gt;, to avoid &lt;code&gt;lmtp(1301): Error: conn unix:/run/dovecot/anvil: net_connect_unix(/run/dovecot/anvil) failed: Permission denied&lt;/code&gt; error&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Copying or moving Incus containers between hosts</title><link>https://thomas-leister.de/en/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/en/move-incus-container-between-hosts-zfs/</guid><description>&lt;p&gt;Below, I will briefly explain how to transfer Incus containers between two Incus hosts that are not connected to a shared cluster. In my case, both hosts are equipped with ZFS-based container storage. See also: &lt;a href="https://thomas-leister.de/en/incus-in-vm-with-zfs-pool"&gt;“Running Incus with ZFS in a VM and moving containers to a new storage pool”&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="open-api"&gt;Open API&lt;/h2&gt;
&lt;p&gt;If a container is to be moved between two hosts, a transfer path must first be created between the two Incus hosts. To do this, the API is made available on the network interface on the source host:&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;The IPv6 address in square brackets must, of course, be replaced with the IP address of your own interface. For an IPv4 address, the brackets are omitted.&lt;/p&gt;
&lt;p&gt;Next, a trust token is generated on the source server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus config trust add sourcehost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The source server is added to the target server:&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;The trust token previously issued by the source server must be entered at the end. As soon as&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;appears in the console, the first transfer of the container from the source server to the target server can take place.&lt;/p&gt;
&lt;h2 id="transferring-data"&gt;Transferring data&lt;/h2&gt;
&lt;p&gt;The container&amp;rsquo;s root file system can be transferred in two ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a) In one go, with downtime lasting as long as the data transfer. Recommended for small containers.&lt;/li&gt;
&lt;li&gt;b) In several steps via a second, incremental sync. With a few seconds to minutes of downtime. Recommended especially for larger containers with 100+ GB. Only works with file systems such as ZFS that support snapshots.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-simple-transfer-for-smaller-containers"&gt;a) Simple transfer for smaller containers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# (on the source server)
incus stop mycontainer
# (on the target server)
incus copy sourcehost:mycontainer mycontainer --stateless
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="or-b-incremental-transfer"&gt;Or b): Incremental transfer&lt;/h3&gt;
&lt;p&gt;With incremental transfer, the current status of the container is first transferred in the background. The parts that were changed during the transfer (because the container was still in operation) are delivered in a second step. However, this second sync runs much faster because it only transfers a delta and not &lt;em&gt;all&lt;/em&gt; data again.&lt;/p&gt;
&lt;p&gt;Create snapshot on the source server:&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;(The container remains in operation and is not stopped yet)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Start initial transfer to the target server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;incus copy sourcehost:mycontainer mycontainer --stateless
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the first transfer, the source container is terminated and a second, final sync is triggered:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# (on the source server)
incus stop mycontainer
incus snapshot create mycontainer
# (on the target server)
incus copy sourcehost:mycontainer mycontainer --refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, the container is started on the target server:&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="close-api"&gt;Close API&lt;/h2&gt;
&lt;p&gt;Finally, the API on the source server can be closed again if it is not to be used for other purposes:&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>Running Incus with ZFS in a VM and moving containers to a new storage pool</title><link>https://thomas-leister.de/en/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/en/incus-in-vm-with-zfs-pool/</guid><description>&lt;p&gt;For many years, I have been relying on the virtualization and container management tool &amp;ldquo;Incus&amp;rdquo; (formerly &amp;ldquo;LXD&amp;rdquo;) to host my services. Incus runs in a virtual machine and helps me to create separation at the application level. For example, there is an Incus container for trashserver.net, another for metalhead.club, etc. The root file systems of the individual containers are located in a ZFS file system. This allows me to create space-saving snapshots of my containers before critical maintenance actions, e.g., before updates or operating system upgrades.&lt;/p&gt;
&lt;p&gt;Since I recently upgraded the underlying storage, I would like to briefly introduce my storage setup and document for myself (but also for you ;-) ) what I paid attention to and how I moved my containers to the new storage.&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;At the hypervisor level (i.e., on the physical server), storage for my containers is provided in the form of LVM volumes. This allows me to offer the large storage space in many individual volumes for other VMs as well and to dimension it according to resource requirements.&lt;/p&gt;
&lt;p&gt;An LVM volume (block device) is then passed to the VM via libvirt/QEMU and the virtio-blk driver. I use the following parameters for storage in libvirt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Driver: virt-blk&lt;/li&gt;
&lt;li&gt;Cache mode: none (cache is already handled by zfs)&lt;/li&gt;
&lt;li&gt;io = native (usually better than io=threads)&lt;/li&gt;
&lt;li&gt;discard = unmap (for Trim support)&lt;/li&gt;
&lt;li&gt;serial = megastor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I would like to highlight the “serial” setting, which allows you to specify a serial number/ID/unique name for the transferred block device. If you connect multiple storage devices to your VM, each storage device can be identified without any doubt. For example, a device named &amp;ldquo;megastor&amp;rdquo; will appear in the VM as &amp;ldquo;/dev/disk/by-id/virtio-megastor&amp;rdquo;. This is particularly helpful if&amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;hellip; you are no longer sure what is actually between /dev/vdb and /dev/vdd and which storage device is which?&lt;/li&gt;
&lt;li&gt;&amp;hellip; or you want to remove /dev/vdc, but you are afraid that the next time you reboot, /dev/vdd will move up and take its place. Then the chaos would be complete.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is because &lt;strong&gt;device names for storage devices are not &amp;ldquo;stable&amp;rdquo; under Linux!&lt;/strong&gt; Only partition UUIDs are stable (or alternatively disk IDs!).&lt;/p&gt;
&lt;p&gt;In the VM, the block device is used to create a simple ZFS pool, e.g. 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;The ZFS pool is also registered in Incus so that it can be used with the containers:&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;Some time ago, only the virtio-scsi driver supported &lt;code&gt;trim&lt;/code&gt; to mark and release free storage space on the SSD. Regular &amp;ldquo;trimming&amp;rdquo; leads to significantly better performance. However, since some QEMU versions, the leaner “virtio-blk” driver also supports trimming.&lt;/p&gt;
&lt;p&gt;Incidentally, ZFS regularly executes a trim cron job (&lt;code&gt;/etc/cron.d/zfsutils-linux&lt;/code&gt;) itself via the &lt;code&gt;zfsutils-linux&lt;/code&gt; package, so it is not necessary to execute &lt;code&gt;fstrim&lt;/code&gt; yourself.&lt;/p&gt;
&lt;p&gt;However, it is important to note that, as mentioned above, &lt;code&gt;discard=unmap&lt;/code&gt; is defined for the QEMU storage device. And in my case, a &amp;ldquo;discard&amp;rdquo; parameter must also be entered in &lt;code&gt;/etc/crypttab&lt;/code&gt; at the hypervisor level due to LUKS encryption. Otherwise, the trimming will not be passed through to our storage layer. Example:&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;In addition, LVM should also be configured appropriately: &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="moving-existing-incus-vms-to-new-storage"&gt;Moving existing Incus VMs to new storage&lt;/h2&gt;
&lt;p&gt;As mentioned at the beginning, I installed new SSDs in the system and set up new ZFS pools accordingly, so now the Incus containers should also be moved to the new &amp;ldquo;megastor&amp;rdquo; pool. There are several methods for doing this&amp;hellip;&lt;/p&gt;
&lt;h3 id="the-easy-way-with-a-few-minutes-of-downtime"&gt;The easy way with a few minutes of downtime&lt;/h3&gt;
&lt;p&gt;This is how it works with Incus:&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;Before the transfer, the container is stopped, which takes a certain amount of time depending on the size of the container and the performance of the storage, thus causing downtime. However, this method is less complex. It is worthwhile for smaller containers with a size of around 50–100 GB.&lt;/p&gt;
&lt;h3 id="with-incremental-transfer--but-short-downtime"&gt;With incremental transfer – but short downtime&lt;/h3&gt;
&lt;p&gt;If you want to avoid downtime as much as possible, you can proceed differently:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a snapshot of the source container during operation&lt;/li&gt;
&lt;li&gt;Copy the snapshot to the destination in the background&lt;/li&gt;
&lt;li&gt;Stop the source container (start of downtime)&lt;/li&gt;
&lt;li&gt;Transfer the difference since the last transfer (usually only a few MB to GB)&lt;/li&gt;
&lt;li&gt;Remove the old container and rename the new container.&lt;/li&gt;
&lt;li&gt;Start the new container (end of downtime).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Downtime is reduced to a minimum. I have already described the basic procedure in my article &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; and have worked around LXD, so to speak. However, the same also works with Incus on-board tools and can be applied to the transfer of a container from one storage to another (not just to the transfer between two hosts!).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# 1. Create a snapshot during operation
incus snapshot create mycontainer
# 2. Transfer the snapshot to the new storage “megastor”
incus copy mycontainer new-mycontainer --storage megastor
# 3. Stop the container and create another snapshot
incus stop mycontainer
incus snapshot create mycontainer
# 4. Transfer changes since the start of the first transfer
incus copy mycontainer new-mycontainer --storage megastor --refresh
# 5. Delete old container, rename new container
incus delete mycontainer
incus move new-mycontainer mycontainer
# 6. Start new container
incus config unset mycontainer volatile.apply_template
incus start mycontainer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;incus config unset...&lt;/code&gt; ensures that the new container retains the old MAC addresses, IPs, etc., and is not assigned new addresses.&lt;/p&gt;
&lt;p&gt;If you want, you can also insert additional &amp;ldquo;snapshot - copy&amp;rdquo; sequences between steps 2 and 3:&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;This is particularly useful if a lot of time has passed since the first transfer. If a lot has changed in the container during this time, the final sync will also take significantly longer and result in longer downtime. This can be counteracted by approaching it in several &amp;ldquo;snapshot - copy&amp;rdquo; steps. It is important not to forget the &lt;code&gt;--refresh&lt;/code&gt; flag after the first &lt;code&gt;copy&lt;/code&gt; command.&lt;/p&gt;</description></item><item><title>Transfer Mastodon S3 storage from self-hosted Minio to Hetzner S3</title><link>https://thomas-leister.de/en/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/en/mastodon-s3-storage-von-minio-zu-hetzner-s3-uebertragen/</guid><description>&lt;p&gt;On behalf of a customer, I recently had the task of transferring an S3 bucket for a Mastodon instance from a self-hosted Minio instance to Hetzner S3. I would like to explain the procedure and the configurations I used here.&lt;/p&gt;
&lt;p&gt;The client&amp;rsquo;s Mastodon instance had previously been supplied with media and media cache by a Minio instance on the same server. However, the media cache continued to grow as the Mastodon instance became increasingly networked. Even by reducing the cache retention period to a few days, the costs for S3 storage were too high. This was because it was located on a powerful but relatively expensive external volume of a Hetzner cloud server.&lt;/p&gt;
&lt;p&gt;At the end of 2024, Hetzner introduced Hetzner S3 storage to its product portfolio, opening up an attractive alternative to the Minio instance. An alternative that stands out above all for its low cost. The cost for a 300 GB volume was just under €16/month. Not very much for a business – but certainly a lot for a small, donation-funded Mastodon instance. In comparison, the price for 1000 GB (!) of Hetzner S3 storage is just under €6/month. So the decision to switch was quickly made.&lt;/p&gt;
&lt;h2 id="step-1-parallel-operation-of-two-buckets"&gt;Step 1: Parallel operation of two buckets&lt;/h2&gt;
&lt;p&gt;To make the transition to the new storage as smooth as possible for users and to keep downtime to a minimum, I decided to run both buckets in parallel during the data migration.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Old bucket “minio”: Delivers the media files stored to date (read-only)&lt;/li&gt;
&lt;li&gt;New bucket “hetzner-s3”: Receives new media files, stores them, and delivers them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The upstream Nginx proxy was configured so that the new bucket was queried first when a file request was made. If the requested file could not be found there, the old S3 bucket was queried.&lt;/p&gt;
&lt;p&gt;Proxy configuration before:&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;Proxy configuration afterwards - with parallel operation of both buckets (fallback to old 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;The new &amp;ldquo;location&amp;rdquo; block &amp;ldquo;@oldbucket&amp;rdquo; and the reference to it if files cannot be found in the new bucket are decisive here:&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;It is important at this point not only to catch 404 errors, but also 403 errors (“forbidden”). Hetzner S3 Storage apparently responds with this error code when a resource is requested whose parent directories do not (yet) exist.&lt;/p&gt;
&lt;h2 id="step-2-saving-new-media-in-the-new-bucket"&gt;Step 2: Saving new media in the new bucket&lt;/h2&gt;
&lt;p&gt;Up to this point, users of the Mastodon instance should hardly have noticed anything. The proxy change takes effect in a fraction of a second and is completely transparent to the user. However, there will be a brief downtime of several seconds as the Mastodon configuration is adjusted and all Mastodon services must be restarted.&lt;/p&gt;
&lt;p&gt;The following settings were used as an example for the new bucket in Mastodon&amp;rsquo;s &lt;code&gt;.env.production&lt;/code&gt;:&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;The changes will take effect once the Mastodon services have been restarted. New media files will now be stored exclusively in the new S3 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-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;After a few seconds, Mastodon is ready to go again.&lt;/p&gt;
&lt;h2 id="step-3-migrate-old-data-to-new-bucket"&gt;Step 3: Migrate old data to new bucket&lt;/h2&gt;
&lt;p&gt;The critical part is now complete. Now you can relax and transfer the old data to the new bucket. To do this, I use the “rclone” tool, which makes it very easy to synchronize two S3 buckets with each other. To do this, configurations for the two S3 providers are first created under &lt;code&gt;~/.config/rclone/rclone.conf&lt;/code&gt;. Here is another example:&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;The endpoint, region, access ID, and access key must, of course, be adapted to the respective bucket. Another important setting is &lt;code&gt;acl = public-read&lt;/code&gt;, which ensures that transferred files have the correct permissions. If this setting is not set correctly, the files may not be publicly readable and the media storage cannot be used in the sense of Mastodon.&lt;/p&gt;
&lt;p&gt;Now follows a series of rclone commands that move the media files from the old &lt;code&gt;minio-s3&lt;/code&gt; to the new &lt;code&gt;hetzner-s3&lt;/code&gt; storage. Please note that the &lt;code&gt;copy&lt;/code&gt; subcommand of &lt;code&gt;rclone&lt;/code&gt; is used here. This does not synchronize deletions, but only transports non-existent files from A to B. Unlike &lt;code&gt;sync&lt;/code&gt;! This would delete newly arrived files in the new bucket. So caution is advised. &lt;code&gt;copy&lt;/code&gt; is the right command for our purpose.&lt;/p&gt;
&lt;p&gt;The most important files first:&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;This is followed by the less important media files. These are only cached media and preview images for websites. If you want to save time and traffic, you can also omit these (or reduce the retention time for these media in Mastodon to a few days in advance—this will significantly reduce the amount of data to be transferred!).&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;This process can take up to half a day, depending on the performance of the servers involved and the size of the bucket.&lt;/p&gt;
&lt;h2 id="step-4-disconnect-the-old-bucket"&gt;Step 4: Disconnect the old bucket&lt;/h2&gt;
&lt;p&gt;Once all data has been transferred, it is advisable to keep the old bucket for a while in case any data has not been transferred correctly. However, you can disconnect the old bucket from the proxy on a trial basis and check whether the Mastodon instance runs correctly without the old bucket. To do this, remove the &lt;code&gt;@oldbucket&lt;/code&gt; location block and comment out (or remove) the following lines:&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;After running &lt;code&gt;systemctl reload nginx&lt;/code&gt;, only the new bucket will be used and the website&amp;rsquo;s functionality can be checked. Don&amp;rsquo;t forget: For serious testing, clear your browser cache!&lt;/p&gt;
&lt;p&gt;If everything is still running smoothly after a few days, the old bucket can be deleted. Done!&lt;/p&gt;</description></item><item><title>Automatic dark mode for Drawio diagrams using CSS filters</title><link>https://thomas-leister.de/en/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/en/dark-mode-for-drawio-diagrams/</guid><description>&lt;p&gt;In my latest blog post “&lt;a href="https://thomas-leister.de/en/nginx-http3-quic-proxy-wrong-virtual-host/"&gt;Nginx HTTP/3 proxy server displays content from the wrong virtual host&lt;/a&gt;,” I included a diagram from Diagrams.net / Drawio, which is an SVG file.&lt;/p&gt;
&lt;p&gt;When you export your diagram from Draw.io, you can choose whether you want to apply a light or dark color theme to the exported graphic. I usually choose the light one here so that the diagram fits well with the light default theme of thomas-leister.de. But how do I deal with this in the dark version of my blog? Users automatically see the dark theme if they have set their operating system accordingly. I wondered if there was an easy way to convert the light SVG graphics to dark versions if necessary. After all, CSS now supports &lt;code&gt;filter&lt;/code&gt;, right?&lt;/p&gt;
&lt;p&gt;Fortunately, I was able to find a simple solution to the problem fairly quickly: &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;. And this is the trick I found:&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;This CSS line first inverts all colors (i.e., from light to dark and vice versa). Black is converted to white (and vice versa), but colors are also inverted. This is not intentional. Therefore, colored content is converted back using a &lt;code&gt;hue-rotate&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I implemented the following in my blog&amp;rsquo;s CSS stylesheet:&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;Only graphics located in an &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; HTML element of the class &lt;code&gt;diagram&lt;/code&gt; are affected by the conversion – and only if the user has selected a dark theme in their operating system settings.&lt;/p&gt;
&lt;p&gt;If I now want to embed an SVG drawing in a blog entry, I do so using the following 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;And this is what the result can look like (top in dark mode – bottom in light mode):&lt;/p&gt;
&lt;p&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 proxy server displays content from the wrong virtual host</title><link>https://thomas-leister.de/en/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/en/nginx-http3-quic-proxy-wrong-virtual-host/</guid><description>&lt;p&gt;At the end of May 2025, I published the &lt;a href="https://metalhead.club"&gt;metalhead.club&lt;/a&gt; song via a &lt;a href="https://thomas-leister.de/bandcamp-alternative-faircamp/"&gt;Faircamp website&lt;/a&gt; at &lt;a href="https://music.metalhead.club"&gt;music.metalhead.club&lt;/a&gt;.
However, not all users were able to follow my links to the Faircamp site without problems. In a few cases, users reported at least one of the following errors to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When users accessed the &lt;a href="https://music.metalhead.club/metalheadclub-anthem/"&gt;album page&lt;/a&gt;: Error 404 - not found&lt;/li&gt;
&lt;li&gt;When users accessed the &lt;a href="https://music.metalhead.club"&gt;main page&lt;/a&gt;: The &lt;a href="https://watch.metalhead.club"&gt;watch.metalhead.club globe&lt;/a&gt; was displayed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After a few attempts, I was able to reproduce the error sporadically myself. In the access logs of the Nginx proxy, I noticed that all erroneous requests were made with HTTP/3.&lt;/p&gt;
&lt;p&gt;Since the &lt;a href="https://blog.cloudflare.com/cloudflare-view-HTTP/3-usage/"&gt;proportion of HTTP/3-compatible web browsers is steadily increasing&lt;/a&gt;, I had decided a few months earlier to use a very recent version of Nginx with HTTP/3 enabled on the server. However, I had not enabled HTTP/3 for music.metalhead.club. This raises the question: &amp;hellip;&lt;/p&gt;
&lt;h2 id="why-is-http3-used"&gt;Why is HTTP/3 used?&lt;/h2&gt;
&lt;p&gt;For my Mastodon instance at metalhead.club, HTTP/3 was enabled as the only virtual host on the Nginx server, primarily to reduce connection setup time for users located further away and thus shorten loading times. For this virtual host, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Alt-Svc"&gt;Alt-Svc header was also set to “h3”&lt;/a&gt; (i.e., HTTP/3). However, this setting did not apply to other virtual hosts such as music.metalhead.club – so why did various web browsers still send HTTP/3-based requests to the non-HTTP/3 virtual host music.metalhead.club?&lt;/p&gt;
&lt;p&gt;To explain this phenomenon, it is important to know that HTTP/3, like its predecessor HTTP/2, has a feature called “connection coalescing.” If &amp;hellip;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP address&lt;/li&gt;
&lt;li&gt;port, and&lt;/li&gt;
&lt;li&gt;SSL certificate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;are the same, &amp;ldquo;old&amp;rdquo; QUIC connections are reused (see also &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; – also known as &amp;ldquo;&lt;a href="https://blog.cloudflare.com/connection-coalescing-experiments/"&gt;Connection Coalescing&lt;/a&gt;&amp;rdquo;). So even though a different virtual host on the server may be addressed later, an &lt;strong&gt;existing QUIC connection to the server is used if the three parameters match&lt;/strong&gt;. Whether this “other” virtual host supports HTTP/3 at all is no longer checked. In my case, the following apparently happened:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A user first visits metalhead.club. The browser recognizes HTTP/3 support via the “Alt-Svc” header&lt;/li&gt;
&lt;li&gt;Further connections to metalhead.club are now established using HTTP/3 – a QUIC connection is set up for this purpose&lt;/li&gt;
&lt;li&gt;The user then navigates to music.metalhead.club. This other virtual host uses the same IP/port/SSL certificate combination (because of the wildcard certificate). The browser recognizes this and uses the existing QUIC connection&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As a result, the web browser now also communicates with the web server using HTTP/3 when accessing music.metalhead.club.&lt;/p&gt;
&lt;h2 id="-but-musicmetalheadclub-does-not-speak-quic"&gt;&amp;hellip; but music.metalhead.club does not speak QUIC!&lt;/h2&gt;
&lt;p&gt;However, there is no QUIC listener in the Nginx configuration for music.metalhead.club. And this is where the second pitfall comes into play: If a virtual host that does not have a QUIC listener is requested via a QUIC connection, Nginx searches for the “default” QUIC virtual host. The “default” vHost is the one that has been explicitly defined as such – or the one that (as in my case) has been implicitly defined by Nginx through the configuration order. In my case, there was only one vHost that speaks QUIC: the vHost for metalhead.club.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requests to music.metalhead.club were ultimately processed by the vHost for metalhead.club because there was no other vHost with QUIC enabled.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Since this vHost is only a proxy that points to another Nginx instance (&amp;ldquo;Nginx app server&amp;rdquo;), the request is forwarded again. The app server Nginx instance in my setup finally receives the request for music.metalhead.club – but cannot do anything with it. After all, it does not have a vHost that could be responsible for music.metalhead.club (see diagram). So once again, a “default” vHost is selected. And since watch.metalhead.club is the first vHost that appears in the configuration of the second Nginx, the request is forwarded to watch.metalhead.club and answered there: The user sees the globe on the home page&amp;hellip; or a 404 error page, because the requested album overview does not exist on watch.metalhead.club, of course.&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="Red: The incorrect route between the two Nginx instances. Green: The intended route. (1) Due to the missing QUIC listener, the metalhead.club vHost is addressed. (2) This in turn now points to the wrong Nginx app server."&gt;&lt;figcaption&gt;
&lt;p&gt;Red: The incorrect route between the two Nginx instances. Green: The intended route. (1) Due to the missing QUIC listener, the metalhead.club vHost is addressed. (2) This in turn now points to the wrong Nginx app server.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="the-solution"&gt;The solution&lt;/h2&gt;
&lt;p&gt;Mystery solved! But how can the problem be fixed?
The core problem is that web browsers assume that QUIC connections can also be reused for other virtual hosts if the IP address, port, and certificate are the same. Therefore, we should create the appropriate conditions for this on the server side. &lt;strong&gt;The solution to the problem is therefore to enable QUIC for &lt;em&gt;all&lt;/em&gt; virtual hosts that share an IP address/port/certificate combination.&lt;/strong&gt; In my case, I also enabled QUIC for music.metalhead.club (and watch.metalhead.club) on the proxy Nginx. This solved the problem.&lt;/p&gt;</description></item><item><title>Speeding up global DNS resolution by avoiding CNAMES</title><link>https://thomas-leister.de/en/accelerating-global-dns-cnames/</link><pubDate>Thu, 19 Jun 2025 10:27:52 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/accelerating-global-dns-cnames/</guid><description>&lt;p&gt;As shown in my article “&lt;a href="https://thomas-leister.de/en/creating-own-small-cdn-for-mastodon-instance-metalheadclub/"&gt;A small CDN for my Mastodon instance metalhead.club&lt;/a&gt;,” I like to use CNAMEs to organize my DNS entries. I usually create a DNS record for each host that maps its hostname to the IP. Then I use one or more CNAME entries to link certain (sub)domains to these CNAMEs depending on the service and purpose. This helps with overview and can make organization easier—especially if IP addresses for hosts need to be changed. Thanks to the chaining of entries, only the last mapping from server hostname to IP needs to be adjusted if the IP address of the target host changes.&lt;/p&gt;
&lt;p&gt;Using the example from the article mentioned above, a CNAME chain might look like this:&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;So first media.metalhead.club is resolved, then metalheadclub-media.cdn.650thz.de, and finally s3.650thz.de. Only then is the IP address for the target host determined. I like this in the sense of order, because the structure makes sense to me in my head. However, as I discovered when analyzing my &lt;a href="https://thomas-leister.de/en/creating-own-small-cdn-for-mastodon-instance-metalheadclub/"&gt;small CDN for metalhead.club&lt;/a&gt;, this is not particularly conducive to performance. This is because the use of CNAMES also has a significant disadvantage: the time required for DNS resolution increases with each CNAME in the chain up to the IP address.&lt;/p&gt;
&lt;p&gt;For example, resolving the second line from the “Answer Section” alone means: first resolve .de (query DNS root server), then query .de nameserver for 650thz.de, then query 650thz.de nameserver for cdn, and finally query the cdn nameserver for metalhead.club-media. Only then is it clear: s3.650thz.de must be resolved. The game starts again until the IP address is finally determined.&lt;/p&gt;
&lt;p&gt;It should be clear that this chain of two CNAME entries takes time to be resolved by the user&amp;rsquo;s DNS resolver. For regional users who are within range of these name servers, the effort is not so significant, as resolving each link in the chain usually only takes a few tenths of a millisecond.&lt;/p&gt;
&lt;p&gt;However, the situation is different for users who (as in the case of metalhead.club) have to access name servers in Europe from the US or Australia, for example. This is because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Root server: Global&lt;/li&gt;
&lt;li&gt;.de name server: 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;My chain involves four zones on four (virtual) name servers. Each one must be queried individually, and only two of them are globally positioned, meaning they can be accessed worldwide with minimal latency. All regional servers are only accessible to those outside the EU with significantly increased runtimes. The fact that two (from their perspective “slow”) regional DNS name servers have to be queried in the case of a US user means that the increased latencies add up very quickly.&lt;/p&gt;
&lt;p&gt;From the perspective of a US user or a user from Asia, it is therefore particularly important that resolution chains are kept as short as possible. This becomes particularly clear when using a tool such as &lt;a href="https://globalping.io"&gt;globalping.io&lt;/a&gt; and looking at the global resolution times in DNS mode:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/images/globalping-unoptimized.png" alt="Screenshot of globalping.io without optimization"&gt;&lt;/p&gt;
&lt;p&gt;While EU users get media.metalhead.club (media-old.metalhead.club in the screenshot) resolved in about 0.004–0.070 seconds, someone in Japan has to wait a lot longer: about 1.5 seconds! Only then can their browser start connecting to the final destination server.&lt;/p&gt;
&lt;p&gt;Of course, this only applies to the first DNS query after the TTL has expired. Subsequent queries are usually answered by the DNS resolver from the cache, so they are answered orders of magnitude faster. However, after the TTL expires, the first request takes another 1.5 seconds! However, given the small number of users in some regions, it cannot be assumed that they use a common DNS resolver. Therefore, it is important to me that uncached requests are also answered quickly.&lt;/p&gt;
&lt;p&gt;So I decided to do without CNAME chains altogether. And this is how it worked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the metalhead.club zone, I delegated the subdomain media.metalhead.club directly to Scaleway&amp;rsquo;s GeoIP nameserver.&lt;/li&gt;
&lt;li&gt;The Scaleway nameserver then resolves directly to an IP address that matches the user&amp;rsquo;s region.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means that CNAMEs are no longer involved in the resolution: the entire resolution is now based solely on zone delegation.&lt;/p&gt;
&lt;p&gt;The whole process could be improved further by taking the following measures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No separation of “normal” DNS nameserver (Core-Networks.de) and GeoIP nameserver (Scaleway): This would eliminate the need for the “media” zone delegation.&lt;/li&gt;
&lt;li&gt;Global accessibility of all nameservers (however, this is a significant cost factor and also requires a new DNS provider).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But even without these two improvements, I was able to significantly reduce resolution time simply by dispensing with CNAMES, as this screenshot shows:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/globale-dns-aufloesung-beschleunigen-cname/images/globalping-optimized.png" alt="Screenshot of globalping.io: Shows resolution time for Japan. Now 676 ms instead of 1.884 seconds."&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s much better: after optimization by dispensing with CNAMES, the resolution of media.metalhead.club now takes only around 0.7 seconds instead of 1.9 seconds!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;By the way: for technical reasons, the main domain metalhead.club resolves to IP addresses anyway. No optimization was necessary here.&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Creating my own small CDN for my Mastodon instance metalhead.club</title><link>https://thomas-leister.de/en/creating-own-small-cdn-for-mastodon-instance-metalheadclub/</link><pubDate>Thu, 19 Jun 2025 08:11:04 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/creating-own-small-cdn-for-mastodon-instance-metalheadclub/</guid><description>&lt;p&gt;Since the big Twitter wave that flooded the Mastodon network and, more broadly, the Fediverse in the fall and winter of 2022, international users have been playing a bigger role for metalhead.club. The service is hosted entirely in Germany, and that was still the case until recently. However, with the increasing number of international members come new challenges: for example, the rapid delivery of content.&lt;/p&gt;
&lt;p&gt;As long as users are mainly located in Germany and Europe, latency times to the “Full Metal Server” in Frankfurt are low. However, the situation is different for users from Canada, the US, and Australia, for example, of whom there are a significant number on metalhead.club. For these users, using metalhead.club was sometimes a bit of a test of patience, as videos and larger images in particular appeared on the website with a slight delay. I can only simulate the situation in the browser, but even a ping of more than 200 ms spoils the fun of scrolling through the timeline in some places.&lt;/p&gt;
&lt;p&gt;Recently, a US user brought a specific problem to my attention when playing a video within the US. He could only stream the iFixit video with constant interruptions. This prompted me to finally consider a CDN – a content delivery network.&lt;/p&gt;
&lt;p&gt;The purpose of such a network is to bring the content available to a user of a platform or website closer to the user, so to speak. This is not just meant metaphorically, but is actually a physical matter: even if we assume that internet nodes, signal repeaters, and other equipment do not introduce additional latency into the system on the route from here to the US, it is well known that light in undersea cables is not infinitely fast. This fact alone means that web content must actually be offered (physically) closer to the customer. A CDN therefore consists of many data center locations or servers that are installed close to users – ideally in metropolitan areas, where you can also benefit from a well-developed network infrastructure. Depending on the use case, this can be in certain regions of the world or even across the globe. The content is mirrored on all (or some) of the servers, depending on requirements. When requesting content, a US user is not redirected to an EU server, but automatically retrieves the content from a server in their vicinity (a US data center). This process usually remains transparent to the user. There are two established methods for determining the location from which content is accessed (and which server is responsible):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Anycast-based CDNs&lt;/li&gt;
&lt;li&gt;“GeoIP”-based CDNs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="anycast-based-cdns"&gt;Anycast-based CDNs&lt;/h2&gt;
&lt;p&gt;The first CDN type is the “gold standard” and enables very elegant (and accurate) routing: In principle, the same public IP address is assigned to several servers scattered around the world. The routing protocol BGP decides which server is actually addressed when a request is made. This protocol essentially determines the paths that an IP packet must take to reach its destination. In most cases, there are several ways to reach the destination. The routing system knows which one involves the lowest costs (or the lowest latency!). In this respect, the work is handed over to the routing system and it is relied upon to find the fastest route through the Internet. That is what it is there for – that is what it was developed for. The route does not always have to be the shortest geographically. If the latency on route A to server A is less than route B to server B, the IP packets are automatically routed to server A. This server then responds. The other servers that can be reached under the same IP address are not aware of the request. Incidentally, such an anycast is also behind all globally available DNS resolvers such as Google DNS, Quad9, or Cloudflare DNS. The IP address is always the same, but in the background, you are redirected to the nearest server without noticing.&lt;/p&gt;
&lt;h2 id="geoip-based-cdns"&gt;GeoIP-based CDNs&lt;/h2&gt;
&lt;p&gt;The term GeoIP refers to a large database – a GeoIP database, such as those offered by companies like &lt;a href="https://www.maxmind.com/en/geoip-databases"&gt;MaxMind&lt;/a&gt; (and used, for example, in my project &lt;a href="https://watch.metalhead.club"&gt;watch.metalhead.club&lt;/a&gt;). The database is fed with publicly available routing information and stores which IP address ranges (and thus ASNs) are assigned to which companies. Once the ownership of an IP address is clear, conclusions can be drawn about the geographical area of use via the assigned company. IPv6 addresses that come from the &lt;code&gt;2003::/19&lt;/code&gt; network (i.e., &lt;code&gt;2003::&lt;/code&gt; to &lt;code&gt;2003:1fff:ffff:ffff:ffff:ffff:ffff:ffff&lt;/code&gt;) belong to Deutsche Telekom, for example. This also makes it clear where a user with such an IP address comes from when requesting content: from Germany. GeoIP databases store these relationships so that they can be queried: “Where does a user with the IP address xxx come from?” The database then provides a more or less accurate answer.&lt;/p&gt;
&lt;h3 id="the-accuracy-of-geoip-based-cdns"&gt;The accuracy of GeoIP-based CDNs&lt;/h3&gt;
&lt;p&gt;My experience with the free offerings of such databases has shown me that this works quite well at the country level, but it is usually not possible to locate users down to the city level. And perhaps that is just as well. There are better and worse databases. The most accurate databases are usually behind a costly subscription model. This is because GeoIP databases need to be maintained. IP addresses and subnets are traded and sold, especially in times of IPv4 address shortages, and sold to other providers. An address range that just belonged to an African mobile phone provider may belong to a small Asian company tomorrow. If the databases are not regularly maintained, the information becomes outdated and worthless.&lt;/p&gt;
&lt;p&gt;And this is precisely where the major disadvantage of GeoIP lies. While Anycast-based CDNs rely on routing systems already knowing the shortest routes (in terms of latency), GeoIP relies on additional collected information. The assignment of IP address &amp;lt;=&amp;gt; owner is not difficult to determine and is public. The difficulty is much more likely to lie in correctly determining the type of use and region of use for an owner. After all, it could be a small business, but it could also be an ISP that only distributes the addresses to its customers. Only the owner or ISP knows how such distribution takes place and how large the geographical area of use is. Additional tracking information can be used to make this assignment more accurate. For example, thanks to smartphones, an online advertising company could combine GPS location and IP address to determine a user&amp;rsquo;s location more accurately. If this GPS information is then sold to the GeoIP provider, the latter can determine the location of the IP address much more accurately. But that&amp;rsquo;s another story&amp;hellip;&lt;/p&gt;
&lt;p&gt;How does a user get forwarded to the right server if Anycast is not used? It&amp;rsquo;s simple: via the Internet&amp;rsquo;s directory service – the DNS.&lt;/p&gt;
&lt;p&gt;If you want to build a GeoIP-based CDN, you need a DNS server that returns the appropriate IP address for the user&amp;rsquo;s region. For each request to the DNS service, the IP address is determined and the appropriate target host is identified in the background based on a GeoIP database. I deliberately write “the IP address” because which IP address is examined depends &amp;hellip;&lt;/p&gt;
&lt;h3 id="resolver-ip-or-edns-client-subnet-ecs"&gt;Resolver IP or EDNS Client Subnet (ECS)?&lt;/h3&gt;
&lt;p&gt;One might assume that the address of the requesting user would be examined, for example, the public IP address of the smartphone or laptop, but this is not the case. This is because the name server responsible for my domain, e.g., metalhead.club, almost never sees this client. It is the DNS resolvers that request the correct IP for a service for the client – not the clients themselves. So all the GeoIP name server can evaluate is the IP address of the resolver.&lt;/p&gt;
&lt;p&gt;In many cases, this means that the user themselves has not been located, but their DNS resolver can be identified. In most cases, for private customers, this is the standard DNS resolver of the customer&amp;rsquo;s ISP, for example Deutsche Telekom. In most cases, the location of the DNS resolver therefore roughly corresponds to the location of the customer – at least when viewed at country level. So if my name server sees an IP that can be assigned to a Deutsche Telekom resolver, I can usually assume that the user making the request is also from Germany.&lt;/p&gt;
&lt;p&gt;However, there are also special cases: namely, globally available and ISP-independent, open DNS resolvers. For example:&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;These often do not allow conclusions to be drawn about a specific country. In order to still be able to roughly locate the customer actually making the request behind the DNS request, there is an EDNS extension called “ECS” (EDNS Client Subnet) that allows additional information to be sent to my name server: namely, the subnet of the original user. The resolver naturally knows this client IP and forwards it to my name server in anonymized form (only the subnet instead of the exact IP address). The name server can now use the subnet forwarded via ECS instead of the resolver IP for its GeoIP database and obtains a more precise result for the geographical origin of the request.&lt;/p&gt;
&lt;p&gt;ECS is not supported by all public resolvers – DNS4EU, for example, does not yet support ECS – even though the feature is on the operator&amp;rsquo;s to-do list:&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="so-what-should-you-use-a-cdn-provider-or-self-hosting"&gt;So what should you use? A CDN provider or self-hosting?&lt;/h2&gt;
&lt;p&gt;Anycast systems are expensive. Very expensive. And impossible for someone like me to implement. Because to do so, you would need to have your own IP address range for which you can define BGP routes yourself. So the issue was quickly settled for me. Anycast? I don&amp;rsquo;t operate it myself. But there is another option: After all, you can rent space in existing Anycast systems or simply rent CDNs. Renting Anycast IPs is also costly and is not offered in this form by the hosts I use or want to use.&lt;/p&gt;
&lt;p&gt;However, freely rentable CDNs are a dime a dozen: BunnyCDN, Akamai, Cloudflare, KeyCDN, &amp;hellip; these are just a few popular examples. These CDNs are often sold in combination with DNS services, S3 storage, or DDoS protection. There are also some offers that are affordable for small platform operators on the internet. I was particularly interested in BunnyCDN. For around €20-25 per month, I could have the approximately 2.5 TB of metalhead.club media played worldwide every month. However, there are two issues that currently concern me:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I would have to relinquish TLS termination and have the media hosted externally. However, my users rely on me to keep all data in my own hands.&lt;/li&gt;
&lt;li&gt;The green electricity issue: It has become easier to find server hosts that operate exclusively with green electricity. Unfortunately, the situation is not so good with CDN providers: Only Cloudflare &lt;a href="https://blog.cloudflare.com/de-de/cloudflare-committed-to-building-a-greener-internet/"&gt;seems to use 100% green electricity&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means that renting space in an existing CDN is also out of the question (for now?).&lt;/p&gt;
&lt;h2 id="building-your-own-geo-ip-based-cdn"&gt;Building your own Geo-IP-based CDN&lt;/h2&gt;
&lt;p&gt;So the only option left is to build it yourself!&lt;/p&gt;
&lt;p&gt;My expectations are relatively low. I am, of course, aware that a self-built GeoIP-based CDN will quickly reach its limits in terms of efficiency, accuracy, and performance. This is especially true if, like me, you plan to invest very little money in it. My primary goal is to improve the user experience for some of my metalhead.club members. I won&amp;rsquo;t achieve the perfect solution this way.&lt;/p&gt;
&lt;p&gt;Since the supply situation within Europe is fine and latency measurements using online tools (more on that later!) have delivered satisfactory results, I have focused on two locations in particular: the American continent and Asia. With a total of three media servers for media.metalhead.club, this would roughly cover the entire globe.&lt;/p&gt;
&lt;p&gt;Using the &lt;a href="https://aaronstanek.com/projects/ping-latency-map"&gt;“Ping Simulation” tool&lt;/a&gt;, I looked at what latencies I can expect globally if I distribute the servers as I had imagined. Namely, as the server host Hetzner allows me to do with its cloud. I already have a Hetzner account and am familiar with their products – plus, they meet my requirements (German company, GDPR-compliant, 100% green electricity). And when it comes to locations, Hetzner offers exactly what I want. In addition to European data centers, there are also two in the US and one in Singapore.&lt;/p&gt;
&lt;p&gt;The simulation showed that I can reach large parts of the world with around 100 ms or less. Of course, this always depends on the local provider, but the overall picture was fairly consistent and only in a few locations were the packet times significantly higher than 150 ms. So I quickly booked a cloud server in Ashburn (VA, USA) and one in Singapore with Hetzner.&lt;/p&gt;
&lt;h2 id="implementation"&gt;Implementation&lt;/h2&gt;
&lt;p&gt;As far as vServers are concerned, I opted for the smallest and cheapest models offered by Hetzner. I wanted to start small and try it out first. As it turns out, the CPX11 cloud servers have been more than sufficient so far. They only need to cache and deliver static files – nothing more.&lt;/p&gt;
&lt;h3 id="nginx-caching-proxy"&gt;Nginx caching proxy&lt;/h3&gt;
&lt;p&gt;Only one Nginx instance runs on the servers, which&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;hellip; receives requests to media.metalhead.club&lt;/li&gt;
&lt;li&gt;checks whether the requested content is already available locally&lt;/li&gt;
&lt;li&gt;(if not, it is requested from the original S3 bucket and cached)&lt;/li&gt;
&lt;li&gt;returns it to the client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My configuration for 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;By the way: Adding the &lt;code&gt;X-Served-By&lt;/code&gt; header makes debugging and checking the function easier. For every image loaded from media.metalhead.club, it is very easy to determine which of the three hosts actually transferred the file. The header can be viewed in the developer tools of any web browser, for example.&lt;/p&gt;
&lt;h3 id="geoip-based-dns-zones"&gt;GeoIP-based DNS zones&lt;/h3&gt;
&lt;p&gt;Now all that&amp;rsquo;s missing is the GeoIP component so that metalhead.club members are redirected to the appropriate server. At this point, I could have configured my own GeoIP/GeoDNS-enabled name server (as I have already done in a customer project), but I didn&amp;rsquo;t want to put too much effort into my experiment. So I looked around a bit and found a very interesting offer at Scaleway. There, you can rent name servers that not only support the GeoIP feature but can also be used with external domains (i.e., domains that are not registered with Scaleway). Depending on usage, such a service costs only a few cents to a low single-digit euro amount in my case. I&amp;rsquo;ll be able to say exactly how much once my CDN has been running for a while ;-)&lt;/p&gt;
&lt;p&gt;However, I didn&amp;rsquo;t want to hand over my 650thz.de root zone to Scaleway. This should continue to be hosted by Core-Networks.de. Instead, I created my own zone “cdn.650thz.de” and set the Scaleway name servers as authoritative name servers. However, I first had to prove ownership of cdn.650thz.de to Scaleway. So I haven&amp;rsquo;t set the NS entries yet, but first created a verification entry in the metalhead.club zone:&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;After about 30 minutes, it was confirmed that I had control over the domain, and I was able to start entering the Scaleway name servers for cdn.650thz.de in the 650thz.de zone at Core-Networks.de:&lt;/p&gt;
&lt;p&gt;cdn 86400 NS ns0.dom.scw.cloud.
cdn 86400 NS ns1.dom.scw.cloud.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(I deleted the &lt;code&gt;_scaleway-challenge&lt;/code&gt; entry again)&lt;/em&gt;&lt;/p&gt;
&lt;div class="tip"&gt;
By the way: I use the 650thz.de domain for all infrastructure in the background, as metalhead.club belongs to the 650thz.de project. Don&amp;rsquo;t let that confuse you.
&lt;/div&gt;
&lt;p&gt;In the &lt;code&gt;cdn.650thz.de&lt;/code&gt; zone, I finally added the GeoIP entry for &lt;code&gt;metalheadclub-media.cdn.650thz.de&lt;/code&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 of DNS input mask at Scaleway"&gt;&lt;/p&gt;
&lt;p&gt;The default entry points to &lt;code&gt;s3.650thz.de&lt;/code&gt; – the previous S3 media server in Frankfurt. However, if a client location in North or South America is detected, the CDN server &lt;code&gt;cdn-us.650thz.de&lt;/code&gt; is used instead. If a location in Asia or the Pacific region is detected, the CDN server &lt;code&gt;cdn-ap.650thz.de&lt;/code&gt; is referenced.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Incidentally, the period at the end of each FQDN in the input mask is essential!&lt;/strong&gt; If no period is set, an entry in the same zone is referenced – which cannot work.&lt;/p&gt;
&lt;div class="tip"&gt;
In retrospect, chaining DNS records using CNAME has proven to be unfavorable for users who are far away. More on this under &lt;a href="https://thomas-leister.de/en/creating-own-small-cdn-for-mastodon-instance-metalheadclub/#further-improvements-dns-optimization"&gt;“Further improvements: DNS optimization”&lt;/a&gt;
&lt;/div&gt;
&lt;h3 id="switching-to-cdn-operation"&gt;Switching to CDN operation&lt;/h3&gt;
&lt;p&gt;In order to respond to all requests to media.metalhead.club with the appropriate media server, one more change was necessary: The entry for &lt;code&gt;media.metalhead.club&lt;/code&gt; had to refer to &lt;code&gt;metalheadclub-media.cdn.650thz.de&lt;/code&gt; in the &lt;code&gt;metalhead.club&lt;/code&gt; zone file:&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;(Again, pay attention to the final point!)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To test the CDN after the DNS change (and the previously entered TTL), I quickly ran an &lt;code&gt;nslookup&lt;/code&gt; on &lt;code&gt;media.metalhead.club&lt;/code&gt; from each of the servers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From s3.650thz.de: Returns own IP&lt;/li&gt;
&lt;li&gt;From cdn-us.650thz.de: Returns own IP&lt;/li&gt;
&lt;li&gt;From cdn-ap.650thz.de: Returns own IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the dynamic allocation worked!&lt;/p&gt;
&lt;h2 id="how-well-does-it-work"&gt;How well does it work?&lt;/h2&gt;
&lt;p&gt;To get a broader picture, I ran further tests using the following tools, for example:&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 with 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;I also asked metalhead.club users about this. A user from Australia had previously reported these ping times to 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;(That&amp;rsquo;s an average of just under 400 ms!)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After that, the latencies were significantly lower because he was correctly redirected to the US server instead of the EU server:&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;(264 ms on average).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Not earth-shattering, but still significantly less. The fact that this user still has a relatively high latency to the media storage is due to the fact that the distance from Australia to Singapore is still not negligible. This is where my small CDN immediately shows its weakness: With only three locations, you can&amp;rsquo;t achieve optimal performance - but you can at least achieve a small improvement.&lt;/p&gt;
&lt;p&gt;For another Australian user, the latency &lt;em&gt;improved from an average of 346 ms to 108 ms&lt;/em&gt;!&lt;/p&gt;
&lt;p&gt;And for the user from the US I mentioned at the beginning? He was able to play the video in question smoothly and without any problems after the switch.&lt;/p&gt;
&lt;p&gt;I used the BunnyCDN test to do a before-and-after comparison:&lt;/p&gt;
&lt;p&gt;Before:&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 shows latency before switching to a CDN"&gt;&lt;/p&gt;
&lt;p&gt;After:&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 shows latency after switching to a CDN"&gt;&lt;/p&gt;
&lt;h2 id="goal-achieved"&gt;Goal achieved!&lt;/h2&gt;
&lt;p&gt;Latency times have improved significantly for most locations around the US, Australia, Korea, and Japan. Europe remains unchanged, as expected. However, there are also some unexpected outliers that may have fallen victim to incorrect IP localization, for example in Singapore itself (interesting!), Turkey, India, the US state of Texas, and Chile.&lt;/p&gt;
&lt;p&gt;Of course, the results also depend heavily on the IP addresses used for testing and the local connection. Turkey may already have been classified as an &amp;ldquo;Asian region,&amp;rdquo; and the location from which testing was conducted in Texas may have been identified as European.&lt;/p&gt;
&lt;p&gt;I will continue to monitor the results. Since the measurements here were primarily taken from data centers (and not from private homes in the corresponding ISP areas), the results may not reflect the whole truth.&lt;/p&gt;
&lt;p&gt;So, one might rightly ask: &amp;hellip;&lt;/p&gt;
&lt;h2 id="can-you-rely-on-geoip"&gt;Can you rely on GeoIP?&lt;/h2&gt;
&lt;p&gt;When testing my CDN with the test websites mentioned above, I discovered that the server contacted didn&amp;rsquo;t always correctly match the test location (see the result for the Singapore location!), so I surveyed my users. The results from these web tools are probably not particularly accurate. I suspect that the mechanism works much better for end-user address ranges than for data center address ranges. After all, GPS and other location information can be &amp;ldquo;fused&amp;rdquo; much more easily for residential IP addresses.&lt;/p&gt;
&lt;p&gt;And indeed: In almost all cases, users were assigned the correct media server based on their origin. See: &lt;a href="https://metalhead.club/@thomas/114676148254141233"&gt;https://metalhead.club/@thomas/114676148254141233&lt;/a&gt; &lt;em&gt;(I probably should have added a poll option to this post&amp;hellip;)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The last manual count on June 15, 2025, showed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For 83 users (most of them from Germany), the allocation worked.&lt;/li&gt;
&lt;li&gt;For 4 users, it didn&amp;rsquo;t work. 3 of them were redirected to the USA instead of Frankfurt - one user was even connected to Singapore, even though they were in Germany.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I asked Scaleway Support which GeoIP database was used and how often it was updated. I received the following response:&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;I also asked whether Scaleway GeoIP uses only the resolver address for positioning, or whether it also evaluates the EDNS ECS field. This would improve accuracy:&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="whats-next"&gt;What&amp;rsquo;s next?&lt;/h2&gt;
&lt;p&gt;An Anycast-based CDN could certainly achieve significantly better results, but for the reasons mentioned above, that&amp;rsquo;s not a viable solution for me at the moment. Since my small GeoIP CDN achieves an improvement in most, but not all, cases, I&amp;rsquo;ll continue the experiment. Perhaps there are still some adjustments I can make to improve localization.&lt;/p&gt;
&lt;p&gt;Otherwise, I&amp;rsquo;m also toying with the idea of ​​possibly switching to a more professional, third-party hosted AnyCast CDN. However, then Cloudflare would be the only provider I could rely on. I&amp;rsquo;m not willing to give up the &amp;ldquo;100% Green Energy&amp;rdquo; label of my services for a CDN.&lt;/p&gt;
&lt;p&gt;Overall, I can say that my GeoIP CDN helps improve access for most remote users. However, there are limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The appropriate CDN server is not assigned in all cases (inaccuracy of the GeoIP database).&lt;/li&gt;
&lt;li&gt;To achieve a latency advantage, a specific content must already have been accessed by another user in the region (content is not (yet?) kept synchronously across all CDN servers =&amp;gt; more storage required).&lt;/li&gt;
&lt;li&gt;Only media content is hosted by the CDN. API services and the web frontend are still hosted only in the EU.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="further-improvements-dns-optimization"&gt;Further Improvements: DNS Optimization&lt;/h2&gt;
&lt;p&gt;After rolling out my CDN, I discovered through performance tools that DNS name resolution, especially for remote users, accounts for a large portion of the overall load time. I described how to improve this in this follow-up article: &amp;ldquo;&lt;a href="https://thomas-leister.de/en/accelerating-global-dns-cnames/"&gt;Speeding ​​up global DNS resolution by eliminating CNAMES&lt;/a&gt;&amp;rdquo;&lt;/p&gt;</description></item><item><title>Icinga DB fails after update</title><link>https://thomas-leister.de/en/icinga2-db-fails-after-update/</link><pubDate>Mon, 24 Mar 2025 16:22:33 +0100</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/icinga2-db-fails-after-update/</guid><description>&lt;p&gt;Since I have been using Icinga DB (instead of just PostgreSQL) as the backend for my Icinga2 instance, it has happened to me twice that Icinga no longer worked correctly after an update of the Icinga DB package. On closer inspection, it also becomes clear why:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;× icingadb.service - Icinga DB
Loaded: loaded (/lib/systemd/system/icingadb.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Wed 2025-01-22 21:42:50 CET; 27s ago
Duration: 13ms
Main PID: 1853805 (code=exited, status=1/FAILURE)
CPU: 13ms
Jan 22 21:42:50 monitor systemd[1]: Starting icingadb.service - Icinga DB...
Jan 22 21:42:50 monitor systemd[1]: Started icingadb.service - Icinga DB.
Jan 22 21:42:50 monitor icingadb[1853805]: Starting Icinga DB daemon (v1.2.1)
Jan 22 21:42:50 monitor icingadb[1853805]: Connecting to database at &amp;#39;pgsql://icingadb@localhost:5432/icingadb&amp;#39;
Jan 22 21:42:50 monitor icingadb[1853805]: unexpected database schema version: v3 (expected v4), please make sure you have applied all database migrations after upgrading Icinga DB
Jan 22 21:42:50 monitor systemd[1]: icingadb.service: Main process exited, code=exited, status=1/FAILURE
Jan 22 21:42:50 monitor systemd[1]: icingadb.service: Failed with result &amp;#39;exit-code&amp;#39;.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Apparently the software was updated with the package update, but the database was left out and not automatically upgraded to the new DB schema. It is unclear to me why this does not happen automatically with an APT update. Fortunately, the problem can be solved quickly by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql -U icingadb icingadb &amp;lt; /usr/share/icingadb/schema/pgsql/upgrades/1.2.1.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(If in doubt, select the most recent .sql file in the directory. In my case this was &lt;code&gt;1.2.1.sql&lt;/code&gt;)_&lt;/p&gt;
&lt;p&gt;&amp;hellip; and then a&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart icingadb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;afterwards. Icinga should be able to connect to the Icinga DB again.&lt;/p&gt;</description></item><item><title>Creating a Windows 10 USB boot medium on Fedora / Linux</title><link>https://thomas-leister.de/en/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/en/creating-a-windows-10-boot-medium-from-fedora-linux/</guid><description>&lt;p&gt;I recently refurbished a used and old Acer Aspire E774 laptop as a donation for the &lt;a href="https://computertruhe.de/spenden/sachspenden/"&gt;Computertruhe e.V.&lt;/a&gt;. Of course, this also involves overwriting the hard disk with random data so that old data can no longer be reconstructed. Because “deleted” is not the same as “securely deleted”. I used the &lt;code&gt;shred&lt;/code&gt; tool in a Fedora Live environment for secure deletion. However, it works just as well with any other Linux distribution.&lt;/p&gt;
&lt;p&gt;Since an Ubuntu on the laptop was not immediately executable or reasonable, because the touchpad did not work and there were always strange problems during boot, I decided to simply reinstall Windows 10 after my deletion. Just as it was running perfectly on the laptop before.&lt;/p&gt;
&lt;p&gt;Without further ado, I downloaded a WIndows 10 64-bit image from Microsoft and wanted to copy it to my USB stick:&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;After the image was burned, I plugged the stick into the laptop and called up the boot manager via one of the F keys at startup. Normally, the boot medium can be adjusted here before the OS starts if the UEFI setting is not already correct. After all, I wanted to boot from the USB stick and not from one of the two built-in hard disks.&lt;/p&gt;
&lt;p&gt;But: Nothing! There was no entry for the USB stick. A look into the BIOS revealed that booting from a USB medium did not have the highest priority, but even after I had changed this, the BIOS or UEFI only showed me “No boot medium found”.&lt;/p&gt;
&lt;p&gt;Oh well. Maybe my boot medium was defective. I tried the Fedora Media Writer - a graphical tool that is included by default in every Fedora installation. The Media Writer can be used to copy Fedora images to USB sticks, as well as any other operating system images. But even with the Fedora Media Writer I was unable to get a bootable USB stick: same mistake. Plugging it into other USB slots was also unsuccessful. Finally, I even tried formatting the USB stick manually and then copying the files manually. I followed these instructions: &lt;a href="https://akolles.de/sonstiges/windows-10-bootstick-erstellen"&gt;“Create Windows 10 boot stick”&lt;/a&gt;. But again no success.&lt;/p&gt;
&lt;p&gt;Finally, I came across the tool &lt;code&gt;woeusb&lt;/code&gt;. I liked the fact that it could be installed from the Fedora package sources and - unlike Unetbootin and other alternatives - ran from the command line. So it also worked in my Wayland environment (unlike Unetbootin!).&lt;/p&gt;
&lt;p&gt;I copied my Windows 10 image to my USB stick as follows:&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; and was successful!&lt;/p&gt;
&lt;p&gt;Now - why did this work and my previous attempts did not? The log from &lt;code&gt;woeusb&lt;/code&gt; provides an explanation:&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;There are two interesting points here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firstly, the USB stick is apparently formatted with FAT (mkfs.fat) and not - as can be read in other instructions - with NTFS&lt;/li&gt;
&lt;li&gt;And: &lt;code&gt;install.wim&lt;/code&gt; is “split”?!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The split of the &lt;code&gt;install.wim&lt;/code&gt; file explains why it did not work for me with the other methods: This step splits the large image file (&amp;gt; 4 GB) into smaller files. This is necessary because the FAT file system can only handle files up to a size of 4 GB.&lt;/p&gt;
&lt;p&gt;And why does Microsoft deliver such a large file in its image, which I tried to copy via &lt;code&gt;dd&lt;/code&gt;? Because the Microsoft Windows 10 ISO is NTFS formatted. And not FAT formatted. NTFS can handle such large files without any problems - which is why I was advised in various instructions to format the USB stick with NTFS rather than FAT.&lt;/p&gt;
&lt;p&gt;However, another peculiarity comes into play here: The Acer laptop apparently does not support NTFS boot media. &amp;hellip; How can that be?&lt;/p&gt;
&lt;p&gt;The laptop originally came onto the market with Windows 8. At that time, the Windows boot medium did not contain any files larger than 4 GB. Accordingly, the FAT file system was sufficient. Consequently, the Acer E774 laptop only supports FAT in the UEFI and not NTFS. This also explains why it booted without any problems from all the live Linux USB sticks I gave it. FAT is still used here.&lt;/p&gt;
&lt;p&gt;To summarize: with the &lt;code&gt;woeusb&lt;/code&gt; tool I finally had success because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Dell laptop only supports FAT media&lt;/li&gt;
&lt;li&gt;The woeusb tool uses FAT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;And makes sure that none of the files is larger than 4 GB!&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the key is &lt;code&gt;woeusb&lt;/code&gt;&amp;rsquo;s “split” function.&lt;/p&gt;
&lt;p&gt;On newer laptops, copying the Win10 image should also work simply via &lt;code&gt;dd&lt;/code&gt;. But for my older laptop I found a great solution with &lt;code&gt;woeusb&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>T-Shirts for my Mastodon instance metalhead.club</title><link>https://thomas-leister.de/en/selling-t-shirts-for-mastodon-metalhead-club/</link><pubDate>Tue, 16 Apr 2024 18:05:00 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/selling-t-shirts-for-mastodon-metalhead-club/</guid><description>&lt;p&gt;Two things are usually in short supply when you run a &lt;a href="https://joinmastodon.org"&gt;Mastodon&lt;/a&gt; instance: Funding and public attention. But both are important so that the instance can continue to operate and - if desired - achieve growth or reach.&lt;/p&gt;
&lt;p&gt;My goal with &lt;a href="https://metalhead.club"&gt;metalhead.club&lt;/a&gt; is to offer a professionally hosted platform for everyone who feels at home in the metal music genre. In order for such a theme-based instance to be viable and its users to benefit from it to the greatest extent, it must achieve a certain level of popularity. Nevertheless, the usual advertising media are only of limited use - either because they do not implement my idea of data protection or because they are currently not readily financially viable. Precious donations have to be used sparingly.&lt;/p&gt;
&lt;p&gt;In addition, the target group is large but relatively specific: while you can probably sell an LED light bulb to anyone, it&amp;rsquo;s not quite so easy to sell a social network for metal fans. The right people have to be addressed in their &amp;ldquo;language&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&amp;hellip; and what could be better than selling something that has a high status in the scene and is highly visible? T-shirts! As band shirts, they can be seen everywhere at home, at festivals or concerts and show which bands the wearer likes to listen to. It should work in a similar way with the Instanz T-shirts: As someone wearing a t-shirt of their Instanz, you can function as a kind of mobile advertising medium and at the same time promote what you yourself find worth supporting - the metalhead.club Mastodon Instanz!&lt;/p&gt;
&lt;p&gt;After some metalhead.club members had been enthusiastically demanding them for a long time, I announced the first T-shirt order campaign in summer 2023 and ordered 100 metalhead.club T-shirts and sold them to the members. Now - in spring 2024 - it was time for the second campaign with a different T-shirt model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In the following, I will describe how I approached the &amp;ldquo;metalhead.club T-shirts&amp;rdquo; project and give a few tips. Perhaps it will help some people who are also thinking about offering merchandise for their Mastodon instance.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/selling-t-shirts-for-mastodon-metalhead-club/images/tom-with-metalhead-club-tshirt.jpg" alt="Tom wearing metalhead.club t-shirt"&gt;&lt;/p&gt;
&lt;h2 id="do-it-yourself-or-lean-back"&gt;Do it yourself or lean back?&lt;/h2&gt;
&lt;p&gt;There are numerous stores on the internet and in cities that offer a wide variety of merchandise and often even ship directly to the buyer. Examples include Spreadshirt, Red Bubble and Printful. In some cases, the suppliers can even be commissioned automatically via online store plugins, so that the seller of the items no longer has to do much: The customer orders in an online store, the supplier carries out the production, processes the payment and sends the goods to the customer. The store operator collects a few euros from the sale at the end.&lt;/p&gt;
&lt;p&gt;However, I want to take a different approach with my T-shirts. As long as the quantities (approx. 100 items per campaign) allow it, I want to be involved in the ordering process myself: I take payment and do the shipping myself - even if it is sometimes tedious work. In this way, I can avoid unnecessary money flowing to third parties in the form of commissions and fees and I am also in a position to enable data protection-friendly purchasing: Only I know the customers and their address data. They can be deleted after use. I can also check the quality myself and choose the packaging, as well as add any extras I like, such as metalhead.club business cards.&lt;/p&gt;
&lt;p&gt;Although I initially toyed with the idea of printing the T-shirts myself, I quickly abandoned the idea. Because in addition to the right equipment, you also need a lot of time and space - all things that I don&amp;rsquo;t have. I have my T-shirts purchased and printed by a supplier. I then sell the result myself.&lt;/p&gt;
&lt;h2 id="the-ingredients-t-shirts-and-screen-printing"&gt;The ingredients: T-shirts and (screen) printing&lt;/h2&gt;
&lt;p&gt;When it comes to merchandise or team clothing with printing, there is a big name in the industry: &lt;a href="https://www.stanleystella.com"&gt;Stanley/Stella&lt;/a&gt;. The company has been producing high-quality clothing since 2012, which can then be further customized by third parties - a kind of &amp;ldquo;blank clothing&amp;rdquo;. A special feature is the organic cotton certification.&lt;/p&gt;
&lt;p&gt;The brand&amp;rsquo;s T-shirts were highly recommended to me in conversation with some metalhead.club members, who confirmed their high quality. In the meantime, I can also give an absolute recommendation - the clothing has proven itself and the price is right. So the name was set.&lt;/p&gt;
&lt;p&gt;The choice of model is a matter of taste. The first order campaign focused on the &amp;ldquo;&lt;a href="https://www.stanleystella.com/de-de/stanley-presenter-sttm562"&gt;Presenter&lt;/a&gt;&amp;rdquo; and &amp;ldquo;&lt;a href="https://www.stanleystella.com/de-de/search/stella-evoker-sttw023"&gt;Evoker&lt;/a&gt;&amp;rdquo; models - for the second campaign, I limited myself to the &amp;ldquo;&lt;a href="https://www.stanleystella.com/de-de/crafter-sttu170"&gt;Crafter&lt;/a&gt;&amp;rdquo; model to avoid additional effort. I also limited the color to black in order to have only the dress sizes as the only variable. This makes processing easier and therefore less error-prone. It is advisable to order a few more pieces of all sizes. This may be to compensate for quality defects, to replace lost items or simply to achieve a better unit price with a larger quantity. I got rid of surplus T-shirts relatively quickly during the 2023 order campaign, as not everyone who was interested found out about the campaign or missed it for other reasons.&lt;/p&gt;
&lt;p&gt;In terms of the printing process, it was clear to me pretty quickly that I wanted a screen print. In comparison, it is not necessarily the cheapest or fastest method, but it promises a very high durability, especially with frequent washing, as the color can penetrate deep into the fabric and hold there well. What would a metalhead.club T-shirt be that can&amp;rsquo;t withstand Mosphits and is no longer legible after a few washes?&lt;/p&gt;
&lt;p&gt;I have my T-shirts printed by &lt;a href="https://promoyard.de/"&gt;Promoyard&lt;/a&gt; - a regional company that impressed me from the very first order. They print with OekoTex-certified, skin-friendly inks.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/selling-t-shirts-for-mastodon-metalhead-club/images/tshirts-in-a-box.jpg" alt="T-Shirts in a box"&gt;&lt;/p&gt;
&lt;h2 id="the-ordering-process"&gt;The ordering process&lt;/h2&gt;
&lt;p&gt;Once I had decided on the T-shirt manufacturer and printer, I started to advertise the campaign. On the one hand, I wrote a &lt;a href="https://blog.650thz.de/posts/metalheadclub-tshirts-2024/"&gt;post&lt;/a&gt; on my &lt;a href="https://blog.650thz.de/"&gt;650thz.de Services Blog&lt;/a&gt; summarizing the information about the T-shirts and ordering information - on the other hand, I also wrote posts about the campaign on Mastodon and posted an admin announcement for my instance metalhead.club. Every member will be informed about the T-shirt promotion via a separate message in the web and app.&lt;/p&gt;
&lt;p&gt;After the first order campaign in summer 2023, it was clear that I would have to draw attention to the campaign more frequently in order to reach as many interested parties as possible. Although the order period was 6 weeks long, not everyone found out about the order campaign due to the vacation period and relatively concentrated information.&lt;/p&gt;
&lt;p&gt;The second time in spring 2024, I sent out a post every morning at the beginning to reach members (at least in Germany) in the morning before work or on the way if they had time for Mastodon. Apparently I was still able to reach some interested parties this way. I reduced my tips towards the end of the six-week order period so as not to bore anyone, but posted more shortly before the end of the period to catch those who made a last-minute decision.&lt;/p&gt;
&lt;p&gt;Technically, I handled the orders very simply: Anyone who was interested in a T-shirt wrote me an e-mail with the number of T-shirts, size and address. The customer was then sent an e-mail with a confirmation of receipt and the bank details as well as a Stripe.com link for payment. I manually reconciled and noted the payments on a daily basis. Once payment had been received, another payment confirmation was sent by email. I recorded all orders and the payment status in an unspectacular LibreOffice Calc spreadsheet.&lt;/p&gt;
&lt;p&gt;After the first T-shirt campaign at the latest, I had a simple Django-based order management interface in mind for a while, but I didn&amp;rsquo;t have enough time to develop it. For orders in the order of 100 pieces, a simple spreadsheet application was also fine - albeit less practical.&lt;/p&gt;
&lt;p&gt;Incidentally, I chose to pay in advance to minimize the risk on my side, for example through late or non-payment. Fortunately, the members on metalhead.club (and those who ordered from other instances) proved to be extremely conscientious and reliable. There were no problems with payment.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/selling-t-shirts-for-mastodon-metalhead-club/images/pile-of-packages-on-desk.jpg" alt="Pile of packages"&gt;&lt;/p&gt;
&lt;h2 id="the-dispatch"&gt;The dispatch&lt;/h2&gt;
&lt;p&gt;Once the orders had been collected and the order period had ended, I commissioned my supplier to produce the T-shirts. The T-shirts were delivered within about 2 weeks and I set about sending them out to the members one by one.&lt;/p&gt;
&lt;h3 id="shipping-material"&gt;Shipping material&lt;/h3&gt;
&lt;p&gt;I put a lot of thought into the shipping material, because it should not only get the goods to their destination dry and safe, but also be as environmentally friendly and reusable as possible. With &lt;a href="https://www.biobiene.com/"&gt;Biobiene.com&lt;/a&gt; I have found exactly the right supplier for this! The store offers various grass paper-based shipping materials and other environmentally friendly alternatives.&lt;/p&gt;
&lt;p&gt;The following materials were used for the first metalhead.club T-shirt campaign:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.biobiene.com/plastikfrei-verpacken-kleiderschutzhuellen-aus-papier-din-a3?c=2029"&gt;Paper garment covers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.biobiene.com/graspapier-versandtasche-e-commerce-250x353x50-mm-100-stueck.html"&gt;Grass paper mailing bags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.biobiene.com/verpackungsmaterial-gruene-lieferscheintaschen-aus-papier-din-lang"&gt;Grass paper delivery note envelopes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.biobiene.com/versandmaterial-druckeretiketten-heisap-hei027?c=2048"&gt;Printable address labels&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With their DIN A3 dimensions, the garment covers are a little too large, but they are all the more convenient to fill with the T-shirts. The alternative - &lt;a href="https://www.biobiene.com/alternative-polybeutel-umweltfreundlich-fscpapier-fb300400?c=2029"&gt;&amp;ldquo;polybags&amp;rdquo; made of paper&lt;/a&gt; are cheaper per item, but can only be bought in quantities of 250 or more. So I opted for the former.&lt;/p&gt;
&lt;p&gt;The grass paper mailing bags can be used a second time after being torn open: A second, internal adhesive strip allows them to be used several times.&lt;/p&gt;
&lt;p&gt;The delivery note envelopes are intended for invoices, which are required for customs in the case of international shipments (non-EU countries). A customs declaration must also be affixed to the shipping envelopes - with details of the contents, weight and value.&lt;/p&gt;
&lt;p&gt;I initially printed out the recipient addresses on paper and stuck them on, but unsurprisingly this turned out to be very time-consuming. Later, I ordered printable stickers in A4 size, printed the addresses on them and cut them out. When entering several addresses online, Deutsche Post combines all the addresses into one PDF page (max. 10 addresses per page) and offers this for printing. Once printed, the addresses can then be cut out and stuck on directly.&lt;/p&gt;
&lt;p&gt;For the second campaign, I also wanted to use environmentally friendly address labels and ordered &lt;a href="https://www.biobiene.com/versandmaterial-druckeretiketten-heisap-hei027?c=2048"&gt;Heisap paper labels HEI027&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="shipping-rates-with-deutsche-post-and-dhl"&gt;Shipping rates with Deutsche Post and DHL&lt;/h3&gt;
&lt;p&gt;I have used Deutsche Post for shipping to recipients in Germany. The &lt;a href="https://www.deutschepost.de/de/w/buecherundwarensendung.html"&gt;Warensendung 500&lt;/a&gt; can also be used to send 3 T-shirts in one envelope without any problems.&lt;/p&gt;
&lt;p&gt;I have sent shipments to other EU countries and worldwide via DHL. The &amp;ldquo;Päckchen EU / or International XS 2 kg&amp;rdquo; is suitable for this.&lt;/p&gt;
&lt;p&gt;Shipments within Germany were usually delivered to the recipients within 3 working days. However, international shipments sometimes took several weeks. Patience was particularly required for shipments to Canada and Mexico. But items to Spain and Hungary also seemed to be lost at first, before they were found in post offices many days after the expected date of receipt. It is therefore worth being patient (and, if necessary, instructing the recipient to ask again and have the parcel searched for).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/selling-t-shirts-for-mastodon-metalhead-club/images/deutsche-post-box.jpg" alt="Many packages in two Deutsche Post transport boxes"&gt;&lt;/p&gt;
&lt;h2 id="sending-the-t-shirts"&gt;Sending the T-shirts&lt;/h2&gt;
&lt;p&gt;As soon as the T-shirts and the shipping material have arrived, you can get started. Last time, I stretched out the dispatch of the almost 100 T-shirts over approx. 3 weeks so as not to lose concentration. This is because mistakes in shipping can quickly become relatively expensive - especially when it comes to recipients abroad.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The 2023 T-shirt campaign was a complete success and the order phase for the spring 2024 campaign ended last week. I&amp;rsquo;m looking forward to seeing the many photos of metalhead.club members with their T-shirts! It&amp;rsquo;s nice to see the members in a kind of common uniform in the photos!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://thomas-leister.de/selling-t-shirts-for-mastodon-metalhead-club/images/tshirts-with-business-cards.jpg" alt="T-Shirts with metalhead.club business cards on top"&gt;&lt;/p&gt;</description></item><item><title>Switching Mastodon from Scaleway S3 to self-hosted Minio S3 media storage</title><link>https://thomas-leister.de/en/switching-mastodon-from-scaleway-to-selfhosted-minio-s3/</link><pubDate>Sun, 14 May 2023 19:00:00 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/switching-mastodon-from-scaleway-to-selfhosted-minio-s3/</guid><description>&lt;p&gt;Since the user flood of November 2022 I&amp;rsquo;ve been using Scaleway&amp;rsquo;s S3 storage for media file caching and storage of my &lt;a href="https://metalhead.club"&gt;metalhead.club Mastodon instance&lt;/a&gt;. It was easy to set up and has been working reliably for me. Back then the media cache size increased so much that my server&amp;rsquo;s internal storage could not keep up with the increasing demand. I didn&amp;rsquo;t want to shrink down the cache duration too much and therefore left it at 14 days. At the time, the cache was about 800 GB in size - a big mass of image files that could not be handled by my &lt;a href="https://blog.mydomain.tld/posts/new-server-specs/"&gt;aged server&lt;/a&gt; itself.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://metalhead.club"&gt;metalhead.club&lt;/a&gt; was moved to a more powerful, new server &lt;a href="https://blog.mydomain.tld/posts/new-server-next-steps/"&gt;in April&lt;/a&gt;. The new server was built to have plenty of storage for Mastodon media storage. I planned to move the S3 storage back to my own infrastructure to save costs. Although media file requests have always been cached by my own HTTP proxy and Scaleway did not get any user-related metadata, it also feels better to have more control and not depend on any 3rd party services for such an essential part of my social network software.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(&amp;hellip; and there have been short service interruptions that we&amp;rsquo;ve had several times in the past. The overall experience has been decent, but I hoped to achieve better availability by running S3 myself ;-) )&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As an S3 compatible storage software I chose &lt;a href="https://min.io"&gt;Minio&lt;/a&gt;. I&amp;rsquo;ve never setup and run my own Minio instance before, but I&amp;rsquo;ve heard good things about the project that is usually used in big cloud computing setups. A quick glimpse at the documentation revealed that &lt;a href="https://min.io/docs/minio/container/operations/install-deploy-manage/deploy-minio-single-node-single-drive.html"&gt;running my own simple Minio instance&lt;/a&gt; would just be a matter of minutes - given that the software can easily deployed via Docker. The &amp;ldquo;single node - single drive&amp;rdquo; operating mode of Minio is sufficient for now. I&amp;rsquo;m not expecting any exponential growth of my Mastodon instance anywhere in the future, so I kept the setup as small and simple as possible. No redundancies (already covered by a lower-layer storage backend) and just a single node.&lt;/p&gt;
&lt;p&gt;This article does not aim to be a complete guide, because several days have passed since I implemented my S3 server and I might not remember every detail. Nevertheless I&amp;rsquo;d like to put some notes about the setup here. Just in case it is interesting for anybody.&lt;/p&gt;
&lt;h2 id="basic-minio-setup-using-podman"&gt;Basic Minio setup using Podman&lt;/h2&gt;
&lt;p&gt;Like I mentioned - Minio can bew run as a Docker container. I&amp;rsquo;m not that much a fan of Docker deployments, so chose Podman instead. Podman lets my run Minio &amp;ldquo;root-less&amp;rdquo;, which adds extra security. As the &lt;code&gt;root&lt;/code&gt; user, I installed Podman and created a new user for Minio on my system:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt install podman uidmap slirp4netns
mkdir /opt/minio
useradd -s /usr/sbin/nologin --home-dir /opt/minio minio
chown minio:minio /opt/minio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now create a directory for Minio to store its data, e.g.:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir /var/lib/minio
sudo chown minio:minio /var/lib/minio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;minio&lt;/code&gt; user is not allowed to log in via a login shell. But you can switch to it by using this command as root:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;su -s /bin/bash - minio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;minio&lt;/code&gt; user will run the Podman-managed container. As the &lt;code&gt;minio&lt;/code&gt; user, the Podman container for Minio is downloaded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;podman pull quay.io/minio/minio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see this message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Error: command required for rootless mode with multiple IDs: exec: &amp;ldquo;newuidmap&amp;rdquo;: executable file not found in $PATH&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;log out from your account and log back in. The problem should be fixed then. If not, make sure that package &lt;code&gt;newuidmap&lt;/code&gt; is installed.&lt;/p&gt;
&lt;p&gt;Next the environment variable file is created. It will be located on the container host, but be mounted inside the Minio container so Minio can read it and configure itself according to it.&lt;/p&gt;
&lt;p&gt;Create the file &lt;code&gt;/opt/minio/config.env&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MINIO_SERVER_URL=&amp;quot;https://s3.mydomain.tld&amp;quot;
MINIO_DOMAIN=&amp;quot;s3.mydomain.tld&amp;quot;
MINIO_ROOT_USER=&amp;quot;myadminuser&amp;quot;
MINIO_ROOT_PASSWORD=&amp;quot;mysupepoassword&amp;quot;
MINIO_REGION=myregion
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MINIO_SERVER_URL&lt;/code&gt;: Public URL of your S3 service&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MINIO_DOMAIN&lt;/code&gt;: Domain part of the Public URL. Used my Minio to enable addressing Buckets via subdomain, e.g. &lt;code&gt;mybucket.s3.mydomain.tld&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MINIO_ROOT_USER&lt;/code&gt; and &lt;code&gt;MINIO_ROOT_PASSWORD&lt;/code&gt;: Admin user and password for Minio Console.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MINIO_REGION&lt;/code&gt;: &amp;ldquo;Availablity region&amp;rdquo; of this S3 server. I used my physical server&amp;rsquo;s hostname here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Afterwards, it&amp;rsquo;s time to start up the container for the first time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;podman run -dt \
-p 9000:9000 \
-p 9090:9090 \
-v /var/lib/minio:/mnt/data \
-v /opt/minio/config.env:/etc/config.env \
-e MINIO_CONFIG_ENV_FILE=/etc/config.env \
--name minio \
quay.io/minio/minio \
server /mnt/data --console-address &amp;quot;:9090&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/var/lib/minio&lt;/code&gt; is the path to the Minio S3 storage. In my case it&amp;rsquo;s just the path to the mount point of an ext4 formatted virtual drive. All S3 files will reside there.&lt;/p&gt;
&lt;p&gt;After starting up the container you should already be able to see container details via &lt;code&gt;podman ps&lt;/code&gt; and connect to the admin console at &lt;a href="http://s3.mydomain.tld:9090"&gt;http://s3.mydomain.tld:9090&lt;/a&gt; and log in as your admin user (given that no firewall is blocking access).&lt;/p&gt;
&lt;h2 id="letting-systemd-start-the-container"&gt;Letting systemd start the container&lt;/h2&gt;
&lt;p&gt;Podman is a daemon-less container manager and cannot start or restart containers by itself. To start the Minio container after a reboot, I use a systemd user service that can easily be generated by podman. To make systemd user services work, enable user lingering (switch to &lt;code&gt;root&lt;/code&gt; user for the next two lines):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loginctl enable-linger $(id -u minio)
systemctl start user@$(id -u minio).service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and put the following lines into &lt;code&gt;~/.profile&lt;/code&gt;:
&lt;em&gt;(return to &lt;code&gt;minio&lt;/code&gt; user before, using &lt;code&gt;su -s /bin/bash - minio&lt;/code&gt;!)&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export XDG_RUNTIME_DIR=&amp;quot;/run/user/$(id -u)&amp;quot;
export DBUS_SESSION_BUS_ADDRESS=&amp;quot;unix:path=/run/user/$(id -u)/bus&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, create the user service file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/systemd/user/
podman generate systemd --new --name minio &amp;gt; ~/.config/systemd/user/container-minio.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;hellip; and enable the new Minio service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl --user daemon-reload
systemctl --user enable container-minio.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;if you ever want to modify the &lt;code&gt;podman run&lt;/code&gt; parameters, make your changes in &lt;code&gt;~/.local/systemd/user/container-minio.service&lt;/code&gt; and restart the Minio service using&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl --user daemon-reload
systemctl --user restart container-minio.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your Minio container should automatically start after a reboot from now on.&lt;/p&gt;
&lt;h2 id="nginx-reverse-proxy-config"&gt;Nginx reverse proxy config&lt;/h2&gt;
&lt;p&gt;I use Nginx for handling HTTPS connections. A suitable configuration might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream minio {
server 127.0.0.1:9000;
}
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
# regex: Make bucket subdomains work
server_name ~^([^.]+).s3.mydomain.tld s3.mydomain.tld;
include snippets/tls-common.conf;
ssl_certificate /etc/acme.sh/s3.mydomain.tld/fullchain.pem;
ssl_certificate_key /etc/acme.sh/s3.mydomain.tld/privkey.pem;
# Allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# Disable buffering
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection &amp;quot;&amp;quot;;
chunked_transfer_encoding off;
proxy_pass http://minio; # This uses the upstream directive definition to load balance
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This Nginx virtual host at &lt;code&gt;s3.mydomain.tld&lt;/code&gt; will serve all requests to the Minio S3 server.&lt;/p&gt;
&lt;h2 id="nginx-reverse-proxy-for-minio-console"&gt;Nginx reverse proxy for Minio console&lt;/h2&gt;
&lt;p&gt;There is an extra Nginx virtual host &lt;code&gt;config.s3.mydomain.tld&lt;/code&gt; for the Minio console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name console.s3.mydomain.tld;
include snippets/tls-common.conf;
ssl_certificate /etc/acme.sh/s3.mydomain.tld/fullchain.pem;
ssl_certificate_key /etc/acme.sh/s3.mydomain.tld/privkey.pem;
# Allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# Disable buffering
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
# This is necessary to pass the correct IP to be hashed
real_ip_header X-Real-IP;
proxy_connect_timeout 300;
# To support websockets in MinIO versions released after January 2023
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection &amp;quot;upgrade&amp;quot;;
chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9090; # This uses the upstream directive definition to load balance
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="creating-an-s3-bucket-for-mastodon"&gt;Creating an S3 bucket for Mastodon&lt;/h2&gt;
&lt;p&gt;After logging in to Minio admin dashboard / console at &lt;code&gt;console.s3.mydomain.tld&lt;/code&gt;, I created a new S3 bucket for my metalhead.club &lt;code&gt;metalheadclub-media&lt;/code&gt;. It is important to set the bucket access rules correctly:&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve set a a new policy in the &amp;ldquo;Administrator&amp;rdquo; =&amp;gt; &amp;ldquo;Policies&amp;rdquo; menu. &amp;ldquo;Create Policy&amp;rdquo;, then:&lt;/p&gt;
&lt;p&gt;Name: &amp;ldquo;masto-public-read-nolist&amp;rdquo;&lt;/p&gt;
&lt;p&gt;In the bucket settings, I&amp;rsquo;ve set the &amp;ldquo;Access Policy&amp;rdquo; to &amp;ldquo;Custom&amp;rdquo; and entered this configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&amp;quot;Version&amp;quot;: &amp;quot;2012-10-17&amp;quot;,
&amp;quot;Statement&amp;quot;: [
{
&amp;quot;Effect&amp;quot;: &amp;quot;Allow&amp;quot;,
&amp;quot;Principal&amp;quot;: {
&amp;quot;AWS&amp;quot;: [
&amp;quot;*&amp;quot;
]
},
&amp;quot;Action&amp;quot;: [
&amp;quot;s3:GetObject&amp;quot;
],
&amp;quot;Resource&amp;quot;: [
&amp;quot;arn:aws:s3:::metalheadclub-media/*&amp;quot;
]
}
]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It allows all users from the internet to access the files in the bucket. &lt;strong&gt;But it does not allow to &lt;em&gt;list&lt;/em&gt; the files in the bucket!&lt;/strong&gt;. This is important because once the file list is available to the public, everyone will be able to download the full S3 bucket, which can (and probably will) contain sensitive information that &lt;em&gt;must not&lt;/em&gt; be available to anyone except the information owners. If the file listing is turned off, it it simply not possible to guess all the random URLs and such an attack is avoided. This is the exact &lt;a href="https://basic-tutorials.com/news/major-data-leak-from-mastodon/"&gt;security issue that Mastodon.social&lt;/a&gt; had a while back. We definitely want to avoid that!&lt;/p&gt;
&lt;h2 id="creating-an-api-key-for-mastodon-s3-access"&gt;Creating an API key for Mastodon S3 access&lt;/h2&gt;
&lt;p&gt;The Mastodon software needs a new API key and secret to access the S3 bucket. Other than any anonymous public user, Mastodon must be allowed write access to the S3 bucket. In the Minio console go to &amp;ldquo;Access Keys&amp;rdquo; and create a new key. (Click &amp;ldquo;create&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Copy the API key and secret to your password safe - the secret will never be displayed again!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then click on the API key that was just created to edit its permissions. Set the following permissions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&amp;quot;Version&amp;quot;: &amp;quot;2012-10-17&amp;quot;,
&amp;quot;Statement&amp;quot;: [
{
&amp;quot;Effect&amp;quot;: &amp;quot;Allow&amp;quot;,
&amp;quot;Action&amp;quot;: [
&amp;quot;s3:ListBucket&amp;quot;
],
&amp;quot;Resource&amp;quot;: [
&amp;quot;arn:aws:s3:::metalheadclub-media&amp;quot;
]
},
{
&amp;quot;Effect&amp;quot;: &amp;quot;Allow&amp;quot;,
&amp;quot;Action&amp;quot;: [
&amp;quot;s3:*&amp;quot;
],
&amp;quot;Resource&amp;quot;: [
&amp;quot;arn:aws:s3:::metalheadclub-media/*&amp;quot;
]
}
]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(as always - adapt to your own bucket name!)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Save the bucket key and secret for later - they will be needed inside Mastodon&amp;rsquo;s &lt;code&gt;live/.env.production&lt;/code&gt; file.&lt;/p&gt;
&lt;h2 id="caching-mastodon-media-files"&gt;Caching Mastodon media files&lt;/h2&gt;
&lt;p&gt;But there is another Nginx virtual host specifically for my Mastodon instance: &lt;code&gt;media.metalhead.club&lt;/code&gt;. I&amp;rsquo;ve used the domain for my media files on Scaleway before. My own media proxy does not only allow me to protect my users&amp;rsquo; data towards 3rd party S3 services, but also cache the media files (and thus reducing the load/traffic to my S3 bucket) and make user-transparent S3 migrations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_cache_path /tmp/nginx-cache-metalheadclub-media levels=1:2 keys_zone=s3_cache:10m max_size=10g
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 $scaleway_backend 'https://metalheadclub-media.s3.fr-par.scw.cloud';
set $minio_backend 'https://metalheadclub-media.s3.650thz.de';
# Note: We cannot use both @minio and @scaleway here, because only one named location is accepted with try_files
# Workaround: proxy_intercept_errors section and forwarding to @scaleway if @minio throws 404
# See: https://stackoverflow.com/questions/21286850/nginx-try-files-with-multiple-named-locations
location / {
access_log off;
try_files $uri @minio;
}
#
# Own Minio S3 server (new)
#
location @minio {
limit_except GET {
deny all;
}
resolver 8.8.8.8;
proxy_set_header Host 'metalheadclub-media.s3.650thz.de';
proxy_set_header Connection '';
proxy_set_header Authorization '';
proxy_hide_header Set-Cookie;
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';
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;
#proxy_intercept_errors off;
# 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 'Access-Control-Allow-Origin' '*';
add_header X-Cache-Status $upstream_cache_status;
# Workaround: Forward request to @scaleway if @minio returns 404
proxy_intercept_errors on;
recursive_error_pages on;
error_page 404 = @scaleway;
}
#
# Scaleway S3 server (legacy)
#
location @scaleway {
limit_except GET {
deny all;
}
resolver 8.8.8.8;
proxy_set_header Host 'metalheadclub-media.s3.fr-par.scw.cloud';
proxy_set_header Connection '';
proxy_set_header Authorization '';
proxy_hide_header Set-Cookie;
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';
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 $scaleway_backend$uri;
proxy_intercept_errors off;
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 'Access-Control-Allow-Origin' '*';
add_header X-Cache-Status $upstream_cache_status;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a few notable lines here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;proxy_cache_path&lt;/code&gt; adds a cache directory for requests to any S3 resource. &lt;code&gt;proxy_cache s3_cache&lt;/code&gt; will enable the cache. Thsi reduces load and traffic to the S3 buckets. Since media files are immutable once they are in the S3 storage, we can keep them in the cache them for a long time.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;set $scaleway_backend&lt;/code&gt; and &lt;code&gt;set $minio_backend&lt;/code&gt; set the URLs of the S3 storage backends.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;try_files $uri @minio;&lt;/code&gt; will try to read a file from the local web root (&lt;code&gt;root&lt;/code&gt; directive) first and try the minio backend second.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@minio&lt;/code&gt; and &lt;code&gt;scaleway&lt;/code&gt; &lt;code&gt;location&lt;/code&gt; blocks: There is a proxy configuration for each of the S3 backends. The &lt;code&gt;@minio&lt;/code&gt; &amp;ldquo;named location&amp;rdquo; will point to my own new Minio based S3 service. &lt;code&gt;@scaleway&lt;/code&gt; will point to the 3rd party S3 bucket hosted by my previous S3 provider.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Minio is not able to serve a certain file, Scaleway will be asked to deliver the file.&lt;/p&gt;
&lt;p&gt;By the way: You might think &amp;ldquo;Why not use something like &lt;code&gt;try_files $uri @minio @scaleway;&lt;/code&gt; to implement a Fall-Back to Scaleway? Well, turns out that try_files only supports a &lt;em&gt;single&lt;/em&gt; &amp;ldquo;named location(@)&amp;rdquo;, so this does not work. Instead I used another method to fall back to Scaleway:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Workaround: Forward request to @scaleway if @minio returns 404
proxy_intercept_errors on;
recursive_error_pages on;
error_page 404 = @scaleway;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another important note: the line&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_set_header Host 'metalheadclub-media.s3.650thz.de';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is important since it will set the Host header to a fictional / internal subdomain that is not used in public - but Minio will expect to receive this header so it knows which bucket (&lt;code&gt;metalheadclub-media&lt;/code&gt;) is to be addressed. If the Host header is not set correctly, it will not know and will return an error.&lt;/p&gt;
&lt;h2 id="the-migration-plan"&gt;The migration plan&lt;/h2&gt;
&lt;p&gt;I wanted to make the S3 migration from Scaleway to Minio as seamless as possible. My Mastodon users should not notice the switch - and here&amp;rsquo;s how I did it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I configured Mastodon to use the new Minio based S3 storage for any media uploads (&lt;code&gt;live/.env.production&lt;/code&gt; on the Mastodon server):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;S3_ENABLED=true
S3_BUCKET=metalheadclub-media
AWS_ACCESS_KEY_ID=secretkeyid
AWS_SECRET_ACCESS_KEY=secretaccesstoken
S3_ALIAS_HOST=media.metalhead.club
S3_REGION=hyper2
S3_PROTOCOL=https
S3_HOSTNAME=media.metalhead.club
S3_ENDPOINT=https://s3.650thz.de
S3_SIGNATURE_VERSION=v4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;(then restarted all Mastodon services to apply changes).&lt;/em&gt;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;
&lt;p&gt;I switched the DNS from my old Nginx media proxy to the new one (with Scaleway + Minio backend): The media proxy will now check if the requested media file exists on Minio S3 - if not, it will download from the legacy Scaleway storage. =&amp;gt; New uploads get served by Minio, old ones by Scaleway.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I triggered a S3 files sync using &lt;code&gt;rclone&lt;/code&gt; for media that was uploaded by metalhead.club users (and therefore is not &amp;ldquo;cached remote media&amp;rdquo;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;There will be less S3 requests to Scaleway day after day, since the number of requests to the old Mastodon media cache will decline over time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At some point the Scaleway S3 storage will not be accessed at all anymore, because all media files have either been transferred to Minio (user owned file uploads) or are not relevant anymore (cached media thumbnails, &amp;hellip; - get invalid after a couple of days anyway)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="cloning-existing-instance-user-data-to-the-new-storage"&gt;Cloning existing instance user data to the new storage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Rclone&lt;/code&gt; is an awesome tool for copying files from one S3 bucket to another. I&amp;rsquo;ve created S3 storage configurations for both, Scaleway S3 and Minio S3 by running the &lt;code&gt;rclone config&lt;/code&gt; guide, and here&amp;rsquo;s the result as a text configuration file (in &lt;code&gt;~/.config/rclone/rclone.conf&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[scaleway]
type = s3
provider = Scaleway
env_auth = false
access_key_id = &amp;lt;access_key_id&amp;gt;
secret_access_key = &amp;lt;secret_access_key&amp;gt;
region = fr-par
endpoint = s3.fr-par.scw.cloud
acl = public-read
storage_class = STANDARD
[minio]
type = s3
provider = Minio
env_auth = false
access_key_id = &amp;lt;access_key_id&amp;gt;
secret_access_key = &amp;lt;secret_access_key&amp;gt;
region = hyper2
endpoint = https://s3.650thz.de
location_constraint = hyper2
acl = public-read
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copying the existing user data from Scaleway to Minio was as simple as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rclone copy --progress --transfers=8 scaleway:metalheadclub-media/custom_emojis/ minio:metalheadclub-media/custom_emojis/
rclone copy --progress --transfers=8 scaleway:metalheadclub-media/accounts/ minio:metalheadclub-media/accounts/
rclone copy --progress --transfers=8 scaleway:metalheadclub-media/site_uploads/ minio:metalheadclub-media/site_uploads/
rclone copy --progress --transfers=8 scaleway:metalheadclub-media/media_attachments/ minio:metalheadclub-media/media_attachments/
rclone copy --progress --transfers=8 scaleway:metalheadclub-media/imports/ minio:metalheadclub-media/imports/
rclone copy --progress --transfers=8 scaleway:metalheadclub-media/cache/accounts/ minio:metalheadclub-media/cache/accounts/
rclone copy --progress --transfers=8 scaleway:metalheadclub-media/cache/custom_emojis/ minio:metalheadclub-media/cache/custom_emojis/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I didn&amp;rsquo;t copy Mastodon&amp;rsquo;s &lt;code&gt;cache/preview_cards&lt;/code&gt; and &lt;code&gt;cache/media_attachments&lt;/code&gt; directories on purpose, since these directories make the majority of files and they will lose their validity in a couple of days anyway.&lt;/p&gt;
&lt;p&gt;The file copying took several hours, esp. due to the large amount of files (not the file size!) and was fully transparent to the user. In the mean time, almost every request is served by my own Minio server and the traffic to my Scaleway bucket has drastically declined:&lt;/p&gt;
&lt;figure&gt;&lt;img src="images/scaleway-traffic.png"&gt;&lt;figcaption&gt;
&lt;h4&gt;Scaleway bucket network traffic. Declining after a short period of intense file copying ;-)&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Scaleway bucket will not be needed anymore soon and I will drop it or use it as a backup storage.&lt;/p&gt;</description></item><item><title>acme.sh fails with error 404 - ACME challenge not found (Nginx)</title><link>https://thomas-leister.de/en/acme.sh-fails-with-nginx-error-404/</link><pubDate>Wed, 03 May 2023 17:27:04 +0200</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/acme.sh-fails-with-nginx-error-404/</guid><description>&lt;p&gt;Recently I can into an issue with &lt;code&gt;acme.sh&lt;/code&gt; / Let&amp;rsquo;s Encrypt and a failing ACME validation&lt;/p&gt;
&lt;p&gt;Error 404 when running &lt;code&gt;acme.sh --renew -d mydomain.tld&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Wed May 3 15:31:45 UTC 2023] Pending, The CA is processing your order, please just wait. (1/30)
[Wed May 3 15:31:49 UTC 2023] mydomain.tld:Verify error:&amp;lt;ipaddress&amp;gt; Invalid response from https://mydomain.tld/.well-known/acme-challenge/5GmSwd0P0ukTtX302yHHhAuZMCEDJx7MmAaBBoPIKtk: 404
[Wed May 3 15:31:49 UTC 2023] Please add '--debug' or '--log' to check more details.
[Wed May 3 15:31:49 UTC 2023] See: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="the-problem"&gt;The problem:&lt;/h2&gt;
&lt;p&gt;Probably your Nginx config has two segments:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
...(some redirect to HTTPS)...
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
..etc etc ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="cause-of-the-error"&gt;Cause of the error:&lt;/h3&gt;
&lt;p&gt;acme.sh will only insert its temporary acme-related redirection snippet into the first &amp;ldquo;server&amp;rdquo; section
but not in the second one. Still it will try to connect to the server via HTTPS, first.
There will be no ACME redirection (via temporary inserted &amp;ldquo;location&amp;rdquo; block), so the ACME server will
not find the challenge file (thus: Error 404).&lt;/p&gt;
&lt;h3 id="solution"&gt;Solution:&lt;/h3&gt;
&lt;p&gt;Put everything into one server block, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
# HTTP redirect
if ($scheme = http) {
return 301 https://$server_name$request_uri;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And try again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;acme.sh --renew -d mydomain.tld
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="tip"&gt;
If the renewal keeps failing and you want to try different configurations, make sure to use the &amp;ldquo;staging&amp;rdquo; API of Letsencrypt in the meantime. The main API is rate-limited and will block you after a few tries per hour.
Enable the staging API by using the &lt;code&gt;--staging&lt;/code&gt; flag. If the renewal finally works, go back to the production API to receive a production certificate, e.g. &lt;code&gt;acme.sh --renew --force -d mydomain.tld&lt;/code&gt;.
&lt;/div&gt;
&lt;p&gt;=&amp;gt; Profit :-)&lt;/p&gt;</description></item><item><title>Mastodon: Adding S3 based cloud storage to your instance</title><link>https://thomas-leister.de/en/mastodon-s3-media-storage/</link><pubDate>Wed, 23 Nov 2022 18:19:00 +0100</pubDate><author>thomas.leister@mailbox.org (Thomas Leister)</author><guid>https://thomas-leister.de/en/mastodon-s3-media-storage/</guid><description>&lt;p&gt;My Mastodon instance &lt;a href="https://metalhead.club"&gt;metalhead.club&lt;/a&gt; exists since summer 2016 and seen several waves of new users - but never as many new users as in early November 2022. This has not only led to heavy CPU work on the servers (see my post about &lt;a href="https://thomas-leister.de/en/scaling-up-mastodon/"&gt;scaling up Mastodon&amp;rsquo;s Sidekiq Workers&lt;/a&gt;), but also to greater load on storage space. Mastodon uses a media cache that not only stores copies of preview images for posts containing links - but also copies of all media files that the server knows of. Before the user wave of late 2020 metalhead.club&amp;rsquo;s media cache was about 350 GB in size with a cache retention time of 60 days. Quickly the numbers escalated and after a few days we were already at 400 GB - and after about 3 weeks we had more than 550 GB of cached media files. Not with 60 days retention time - but with 30 only.&lt;/p&gt;
&lt;p&gt;Despite I added hundreds of GB of new storage space, the cache showed no signs of shrinking in the near future, so decided to offload the storage to an S3 storage provider. The local disks would have been full a few days later.&lt;/p&gt;
&lt;p&gt;&amp;hellip; and here&amp;rsquo;s how it works:&lt;/p&gt;
&lt;h2 id="choosing-an-s3-compatible-provider"&gt;Choosing an S3 compatible provider&lt;/h2&gt;
&lt;p&gt;S3 is a storage type / protocol which origins from Amazon, but as I wanted a EU-based company to host my files, I looked for a compatible alternative. Luckily there are several S3 compatible storage providers in the EU, as this list reveals: &lt;a href="https://european-alternatives.eu/alternative-to/amazon-s3"&gt;https://european-alternatives.eu/alternative-to/amazon-s3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My top choices were Scaleway and IONOS. Scaleway is a French Hosting company and since their prices are even more affordable compared to IONOS&amp;rsquo;s prices, I picked Scaleway as my S3 bucket provider. Reviews on Scaleway are very mixed and you might get the impression that their customer service is not very helpful - but for the last 2 weeks my S3 bucket has been running just fine and there was no reason to call for help. We&amp;rsquo;ll see if Scaleway stays my S3 bucket provider or I will switch to IONOS instead.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The following steps will relate to Scaleway S3, but should be more or less applicable to other storage providers as well. You might need to change some details only.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="creating-an-s3-bucket"&gt;Creating an S3 bucket&lt;/h2&gt;
&lt;p&gt;Creating a bucket is easy: Pick a name for your S3 bucket and make it look like a suitable subdomain or directory name. If you name your bucket &amp;ldquo;myinstance-media&amp;rdquo;, you will be able to access the Bucket via those two URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://instance-media.s3.fr-par.scw.cloud"&gt;https://instance-media.s3.fr-par.scw.cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://s3.fr-par.scw.cloud/instance-media"&gt;https://s3.fr-par.scw.cloud/instance-media&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;(&amp;hellip; assuming you&amp;rsquo;ve picket the &lt;code&gt;fr-par&lt;/code&gt; availability region)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Bucket visibility setting should be set to &lt;code&gt;private&lt;/code&gt; instead of &lt;code&gt;public&lt;/code&gt;. This will &lt;strong&gt;not&lt;/strong&gt; change the accessibility of any media file! The &lt;code&gt;private&lt;/code&gt; setting just makes sure that there is not list of file contents available to the public. While we want individual files to be publicly accessible by URL, we don&amp;rsquo;t want to help scrapers by handing them out a list of all available files.&lt;/p&gt;
&lt;h2 id="creating-an-nginx-proxy-for-the-s3-bucket"&gt;Creating an Nginx Proxy for the S3 bucket&lt;/h2&gt;
&lt;p&gt;Usually web browsers would receive the corresponding S3 URL of a media file and directly download the file from one of the URLs mentioned earlier. Because every file would need to be downloaded on every user device at least once, a lot of traffic would be generated - and outgoing traffic costs money. Therefore I implemented an S3 proxy with Nginx. Media assets are not directly downloaded from the S3 storage, but the webbrowsers will download from &lt;a href="https://media.metalhead.club"&gt;https://media.metalhead.club&lt;/a&gt;. If a new media file should be downloaded, the Nginx proxy will pass the request to the S3 storage and cache the contents locally. The next time a request for the same file comes in, Nginx will directly send the cached version of the file instead of re-downloading it from S3. This way a lot of traffic from the S3 bucket can be saved.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my Nginx config:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;proxy_cache_path /tmp/nginx-cache-instance-media levels=1:2 keys_zone=s3_cache:10m max_size=10g
inactive=48h use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name media.metalhead.club;
access_log off;
error_log /var/log/nginx/media.metalhead.club-error.log;
root /home/mastodon/live/public/system;
set $s3_backend &amp;#39;https://instance-media.s3.fr-par.scw.cloud&amp;#39;;
keepalive_timeout 30;
location = / {
index index.html;
}
location / {
try_files $uri @s3;
}
location @s3 {
limit_except GET {
deny all;
}
resolver 9.9.9.9;
proxy_set_header Host &amp;#39;instance-media.s3.fr-par.scw.cloud&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 $s3_backend$uri;
proxy_intercept_errors off;
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;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;(HTTPS is handled by another upstream Nginx instance)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Make sure to update both &lt;code&gt;instance-media.s3.fr-par.scw.cloud&lt;/code&gt; URLs and the FQDN &lt;code&gt;media.metalhead.club&lt;/code&gt; according to your environment. My configuration will keep media files cached for 48 hours before they will be dropped from the cache.&lt;/p&gt;
&lt;p&gt;Two noteworthy lines are &lt;code&gt;root /home/mastodon/live/public/system;&lt;/code&gt; and &lt;code&gt;try_files $uri @s3;&lt;/code&gt;. They will make Nginx first look for a file in the local non-S3 storage. If a file exists there, S3 and the cache will not be tried. This mechanism enables a smooth transition between the local media cache and the remote S3 cache, since data transfer / sync can take hours or even days, depending on the Mastodon media cache size.&lt;/p&gt;
&lt;h2 id="enabling-s3-stroage-support-in-mastodon"&gt;Enabling S3 stroage support in Mastodon&lt;/h2&gt;
&lt;p&gt;I enabled S3 support in Mastodon by editing the &lt;code&gt;.env.production&lt;/code&gt; file in &lt;code&gt;/home/mastodon/live&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S3_ENABLED=true
S3_BUCKET=instance-media
AWS_ACCESS_KEY_ID=&amp;lt;accesskey&amp;gt;
AWS_SECRET_ACCESS_KEY=&amp;lt;secretkey&amp;gt;
S3_ALIAS_HOST=media.metalhead.club
S3_HOSTNAME=media.metalhead.club
S3_REGION=fr-par
S3_ENDPOINT=https://s3.fr-par.scw.cloud
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;accesskey&lt;/code&gt; and &lt;code&gt;secretkey&lt;/code&gt; need to be replaced by proper API access keys. You can find more information about the keys here: &lt;a href="https://www.scaleway.com/en/docs/console/my-project/how-to/generate-api-key/"&gt;https://www.scaleway.com/en/docs/console/my-project/how-to/generate-api-key/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After having modified the config file, &lt;strong&gt;restart all mastodon processes!&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="syncing-your-local-data-to-s3"&gt;Syncing your local data to S3&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s now time to sync your local Mastodon media cache data to your S3 bucket. &lt;a href="https://www.scaleway.com/en/docs/storage/object/api-cli/object-storage-aws-cli/"&gt;Follow the steps of the Scaleway How-To&lt;/a&gt; to set up the &lt;code&gt;aws&lt;/code&gt; tool. When done, run the following steps to sync your data &lt;em&gt;(better use a tmux / screen session - this make take a looong time!)&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /home/mastodon/live
aws s3 sync --acl public-read public/system/ s3://instance-media --endpoint=https://s3.fr-par.scw.cloud
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--acl public-read&lt;/code&gt; is especially important, because uploaded files will only be available publically if the &lt;code&gt;public-read&lt;/code&gt; acl is set for each file individually. If you miss to do that, you will need to re-upload all files. Guess how I learned that &amp;hellip; ;-)&lt;/p&gt;
&lt;p&gt;No worries, if the upload takes many hours. As mentioned before, Nginx will keep serving the media files from the local storage if it doesn&amp;rsquo;t find any suitable file in the S3 bucket, so the transition will be transparent to the users.&lt;/p&gt;
&lt;h2 id="checking-if-it-works"&gt;Checking if it works&lt;/h2&gt;
&lt;p&gt;Apart from the obvious &lt;em&gt;(&amp;ldquo;do all the media files on my instance still load properly?&amp;rdquo;)&lt;/em&gt; you can also check if your web browser fetches images files from the correct location, e.g. media.metalhead.club, and if caching is successful. Open the web developer tools and reload the page while observing the network resources being loaded:&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://thomas-leister.de/en/mastodon-s3-media-storage/images/firefox-developer-tools-cache.en.webp"
alt="A media resource being loaded for the first time"&gt;&lt;figcaption&gt;
&lt;p&gt;A media resource being loaded for the first time&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Check the response headers: The &lt;code&gt;x-cache-status&lt;/code&gt; attribute should be &lt;code&gt;miss&lt;/code&gt; the first time an image os loaded from the server. If you pick the URL of the image and download it in another tab (or reload the page), the attribute should switch to &lt;code&gt;hit&lt;/code&gt;. That means that the image was not delivered by the S3 bucket, but by the local Nginx cache - exactly what we wanted to achieve!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Useful resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.scaleway.com/en/docs/storage/object/api-cli/object-storage-aws-cli/"&gt;Using Object Storage with the AWS-CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stanislas.blog/2018/05/moving-mastodon-media-files-to-wasabi-object-storage/"&gt;Moving Mastodon&amp;rsquo;s media files to Wasabi Object Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cybrespace/cybrespace-meta/blob/master/s3.md"&gt;Getting Mastodon working with Amazon S3 file-hosting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.scaleway.com/en/docs/tutorials/setup-nginx-reverse-proxy-s3/"&gt;Setting up an Nginx reverse proxy with Object Storage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>