<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Matthias Klein</title>
    <link>https://blog.klein.ruhr/</link>
    <description></description>
    <pubDate>Sat, 11 Apr 2026 00:23:07 +0200</pubDate>
    <item>
      <title>From Ghost to WriteFreely: My Migration to the Fediverse</title>
      <link>https://blog.klein.ruhr/from-ghost-to-writefreely-my-migration-to-the-fediverse</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/blogging.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;Why I migrated my blog from Ghost to WriteFreely and how you can do it too&#xA;&#xA;Why WriteFreely?&#xA;&#xA;After years with various blogging platforms, I was looking for something simpler. Ghost was great, but:&#xA;&#xA;✅ Native ActivityPub Integration - My blog is automatically available in the Fediverse&#xA;✅ Minimal Resource Usage - Single binary, no Node.js, no complex infrastructure&#xA;✅ Focus on Writing - No distractions, just writing&#xA;✅ Self-hosted - Full control over my data&#xA;✅ Markdown-native - No formatting battles&#xA;!--more--&#xA;Setup: WriteFreely on LXC (Proxmox)&#xA;&#xA;Why Native Installation Instead of Docker?&#xA;&#xA;After several frustrating hours with Docker mount problems, I decided on native installation. Best decision ever!&#xA;&#xA;Download WriteFreely binary&#xA;cd /opt&#xA;wget https://github.com/writefreely/writefreely/releases/download/v0.15.0/writefreely0.15.0linuxarm64.tar.gz&#xA;tar -xzf writefreely0.15.0linuxarm64.tar.gz&#xA;mv writefreely /opt/writefreely&#xA;cd /opt/writefreely&#xA;&#xA;Interactive configuration&#xA;./writefreely --config&#xA;&#xA;Configuration (config.ini)&#xA;&#xA;[server]&#xA;bind = 0.0.0.0  # Important for reverse proxy&#xA;port = 8080&#xA;https = false   # Reverse proxy handles HTTPS&#xA;&#xA;[app]&#xA;sitename = My Blog&#xA;host = https://blog.example.com  # Your real domain!&#xA;singleuser = true&#xA;federation = true  # This is the key!&#xA;publicregistration = false&#xA;&#xA;[database]&#xA;type = sqlite3  # Simpler than MySQL for single user&#xA;filename = writefreely.db&#xA;&#xA;As systemd Service&#xA;&#xA;Create service file&#xA;sudo tee /etc/systemd/system/writefreely.service &lt;&lt; &#39;EOF&#39;&#xA;[Unit]&#xA;Description=WriteFreely&#xA;After=network.target&#xA;&#xA;[Service]&#xA;Type=simple&#xA;User=www-data&#xA;Group=www-data&#xA;WorkingDirectory=/opt/writefreely&#xA;ExecStart=/opt/writefreely/writefreely&#xA;Restart=always&#xA;RestartSec=10&#xA;&#xA;[Install]&#xA;WantedBy=multi-user.target&#xA;EOF&#xA;&#xA;sudo systemctl daemon-reload&#xA;sudo systemctl enable writefreely&#xA;sudo systemctl start writefreely&#xA;&#xA;Reverse Proxy (Traefik/Nginx)&#xA;&#xA;Example configuration:&#xA;Frontend: blog.example.com&#xA;Backend: http://192.168.1.100:8080&#xA;SSL: Automatic&#xA;&#xA;Migration from Ghost&#xA;&#xA;Export from Ghost&#xA;Ghost Admin → Settings → Labs → &#34;Export content&#34;&#xA;&#xA;Migration Script (Python)&#xA;Since WriteFreely&#39;s import features are limited, I migrated most posts manually. For automatic migration:&#xA;&#xA;!/usr/bin/env python3&#xA;import json&#xA;import requests&#xA;from datetime import datetime&#xA;&#xA;def migrateghosttowritefreely(ghostexport, writefreelyurl, token):&#xA;    with open(ghostexport, &#39;r&#39;) as f:&#xA;        data = json.load(f)&#xA;    &#xA;    posts = data&#39;db&#39;&#39;data&#39;&#xA;    &#xA;    for post in posts:&#xA;        if post[&#39;status&#39;] == &#39;published&#39;:&#xA;            # WriteFreely API Call&#xA;            payload = {&#xA;                &#39;body&#39;: converthtmltomarkdown(post[&#39;html&#39;]),&#xA;                &#39;title&#39;: post[&#39;title&#39;]&#xA;            }&#xA;            # ... API integration&#xA;&#xA;My tip: For small blogs (&lt; 20 posts), manual copying is often faster and cleaner.&#xA;&#xA;Custom CSS: The Perfect Dark Theme&#xA;&#xA;After many iterations, here&#39;s my final WriteFreely dark theme:&#xA;&#xA;/ WriteFreely Custom Dark Theme /&#xA;&#xA;/ === SANS-SERIF FONT === /&#xA;body, html,  {&#xA;    font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, system-ui, sans-serif !important;&#xA;}&#xA;&#xA;/ === ANTHRACITE DESIGN === /&#xA;body, html {&#xA;    background: #2e2e2e !important;&#xA;    color: #f0f0f0 !important;&#xA;}&#xA;&#xA;/ === COLORED HEADING HIERARCHY === /&#xA;body h1 { color: #ffffff !important; font-size: 1.8rem !important; }&#xA;body h2 { color: #7db3f3 !important; font-size: 1.5rem !important; }&#xA;body h3 { color: #98d982 !important; font-size: 1.3rem !important; }&#xA;body h4 { color: #f9cc81 !important; font-size: 1.2rem !important; }&#xA;&#xA;/ === LINKS === /&#xA;body a { color: #7db3f3 !important; }&#xA;body a:hover { color: #98d982 !important; }&#xA;&#xA;/ === CODE BOXES (This was a battle!) === /&#xA;body pre {&#xA;    background: #1e1e1e !important;&#xA;    color: #f0f0f0 !important;&#xA;    border: 1px solid #444 !important;&#xA;    border-radius: 8px !important;&#xA;    padding: 1rem !important;&#xA;}&#xA;&#xA;body code {&#xA;    background: #1e1e1e !important;&#xA;    color: #f0f0f0 !important;&#xA;    border: 1px solid #444 !important;&#xA;    border-radius: 8px !important;&#xA;    padding: 0.2rem 0.4rem !important;&#xA;}&#xA;&#xA;/ === IMAGES WITH ROUNDED CORNERS === /&#xA;body img {&#xA;    border-radius: 12px !important;&#xA;    max-width: 100% !important;&#xA;}&#xA;&#xA;/ === REMOVE FOOTER === /&#xA;body footer { display: none !important; }&#xA;&#xA;HTML Shortcuts for Beautiful Images&#xA;&#xA;For consistent image presentation, I use these HTML shortcuts:&#xA;&#xA;!-- Standard image with shadow --&#xA;&lt;img src=&#34;https://cdn.example.com/images/image.png&#34; alt=&#34;Description&#34; &#xA;     style=&#34;border-radius: 12px !important; &#xA;            box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; &#xA;            margin: 2rem auto !important; &#xA;            display: block !important;&#34;  !-- Image with caption --&#xA;figure style=&#34;text-align: center; margin: 2rem auto;&#34;&#xA;    &lt;img src=&#34;https://cdn.example.com/images/image.png&#34; alt=&#34;Description&#34;&#xA;         style=&#34;border-radius: 12px !important; &#xA;                box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important;&#34;  figcaption style=&#34;color: #8b949e; font-style: italic; margin-top: 1rem;&#34;&#xA;        Caption here&#xA;    /figcaption&#xA;/figure&#xA;&#xA;ActivityPub: Automatically in the Fediverse&#xA;&#xA;The best thing about WriteFreely: It just works!&#xA;&#xA;My blog: @username@blog.example.com&#xA;Followers: Automatically via Mastodon, Pleroma, etc.&#xA;Posts: Automatically federated to the Fediverse&#xA;Interaction: Comments via ActivityPub possible&#xA;&#xA;Testing Federation&#xA;&#xA;Test WebFinger&#xA;curl https://blog.example.com/.well-known/webfinger?resource=acct:username@blog.example.com&#xA;&#xA;ActivityPub profile&#xA;curl -H &#34;Accept: application/activity+json&#34; https://blog.example.com/@username&#xA;&#xA;Troubleshooting: Common Problems&#xA;&#xA;Problem 1: Code boxes stay white&#xA;Solution: Edit WriteFreely&#39;s write.css directly:&#xA;&#xA;cd /opt/writefreely/static/css&#xA;sudo nano write.css&#xA;&#xA;# Search for: background:#f8f8f8&#xA;# Replace with: background:#1e1e1e&#xA;&#xA;Problem 2: CSS not applied&#xA;Solution: Aggressively clear browser cache, test different browsers.&#xA;&#xA;Problem 3: ActivityPub doesn&#39;t work&#xA;Solution: &#xA;federation = true in config.ini&#xA;host = https://your-real-domain.com (not localhost!)&#xA;Reverse proxy configured correctly&#xA;&#xA;Webspace for Images&#xA;&#xA;Separate container for static assets:&#xA;&#xA;docker-compose.yml for webspace&#xA;services:&#xA;  webspace:&#xA;    image: httpd:latest&#xA;    volumes:&#xA;      ./webspace:/usr/local/apache2/htdocs&#xA;    labels:&#xA;      &#34;traefik.http.routers.webspace.rule=(Host(cdn.example.com))&#34;&#xA;      # ... Additional labels&#xA;&#xA;Conclusion: Is the Switch Worth It?&#xA;&#xA;Definitely yes! After migration:&#xA;&#xA;✅ Much faster - Page loads lightning fast&#xA;✅ Less maintenance - One binary, no updates, no Node.js&#xA;✅ Fediverse integration - My posts automatically reach more people&#xA;✅ Focus on writing - No distractions from complex themes/plugins&#xA;✅ Resource efficient - Runs smoothly on small LXC&#xA;&#xA;Resources&#xA;&#xA;WriteFreely: https://writefreely.org&#xA;ActivityPub Plugin (WordPress): https://wordpress.org/plugins/activitypub/&#xA;Fediverse Test: https://fediverse.party&#xA;&#xA;Credits&#xA;Header Image by a href=&#34;https://unsplash.com/@soydanielthomas?utmcontent=creditCopyText&amp;utmmedium=referral&amp;utmsource=unsplash&#34;Daniel Thomas/a on a href=&#34;https://unsplash.com/photos/person-using-macbook-pro-on-black-table-gWlBxOAgXgQ?utmcontent=creditCopyText&amp;utmmedium=referral&amp;utm_source=unsplash&#34;Unsplash/a&#xA;&#xA;Migration completed: ✅ Self-hosted, ✅ Fediverse-ready, ✅ Minimal &amp; fast*]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/blogging.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
<em>Why I migrated my blog from <a href="https://ghost.org">Ghost</a> to <a href="https://writefreely.org">WriteFreely</a> and how you can do it too</em></p>

<h2 id="why-writefreely">Why WriteFreely?</h2>

<p>After years with various blogging platforms, I was looking for something <strong>simpler</strong>. Ghost was great, but:</p>
<ul><li>✅ <strong>Native ActivityPub Integration</strong> – My blog is automatically available in the Fediverse</li>
<li>✅ <strong>Minimal Resource Usage</strong> – Single binary, no Node.js, no complex infrastructure</li>
<li>✅ <strong>Focus on Writing</strong> – No distractions, just writing</li>
<li>✅ <strong>Self-hosted</strong> – Full control over my data</li>

<li><p>✅ <strong>Markdown-native</strong> – No formatting battles
</p>

<h2 id="setup-writefreely-on-lxc-proxmox">Setup: WriteFreely on LXC (Proxmox)</h2></li></ul>

<h3 id="why-native-installation-instead-of-docker">Why Native Installation Instead of Docker?</h3>

<p>After several frustrating hours with Docker mount problems, I decided on native installation. <strong>Best decision ever!</strong></p>

<pre><code class="language-bash"># Download WriteFreely binary
cd /opt
wget https://github.com/writefreely/writefreely/releases/download/v0.15.0/writefreely_0.15.0_linux_arm64.tar.gz
tar -xzf writefreely_0.15.0_linux_arm64.tar.gz
mv writefreely /opt/writefreely
cd /opt/writefreely

# Interactive configuration
./writefreely --config
</code></pre>

<h3 id="configuration-config-ini">Configuration (config.ini)</h3>

<pre><code class="language-ini">[server]
bind = 0.0.0.0  # Important for reverse proxy
port = 8080
https = false   # Reverse proxy handles HTTPS

[app]
site_name = My Blog
host = https://blog.example.com  # Your real domain!
single_user = true
federation = true  # This is the key!
public_registration = false

[database]
type = sqlite3  # Simpler than MySQL for single user
filename = writefreely.db
</code></pre>

<h3 id="as-systemd-service">As systemd Service</h3>

<pre><code class="language-bash"># Create service file
sudo tee /etc/systemd/system/writefreely.service &lt;&lt; &#39;EOF&#39;
[Unit]
Description=WriteFreely
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/writefreely
ExecStart=/opt/writefreely/writefreely
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable writefreely
sudo systemctl start writefreely
</code></pre>

<h3 id="reverse-proxy-traefik-nginx">Reverse Proxy (Traefik/Nginx)</h3>

<p>Example configuration:
– <strong>Frontend</strong>: <code>blog.example.com</code>
– <strong>Backend</strong>: <code>http://192.168.1.100:8080</code>
– <strong>SSL</strong>: Automatic</p>

<h2 id="migration-from-ghost">Migration from Ghost</h2>

<h3 id="export-from-ghost">Export from Ghost</h3>

<p>Ghost Admin → Settings → Labs → “Export content”</p>

<h3 id="migration-script-python">Migration Script (Python)</h3>

<p>Since WriteFreely&#39;s import features are limited, I migrated most posts manually. For automatic migration:</p>

<pre><code class="language-python">#!/usr/bin/env python3
import json
import requests
from datetime import datetime

def migrate_ghost_to_writefreely(ghost_export, writefreely_url, token):
    with open(ghost_export, &#39;r&#39;) as f:
        data = json.load(f)
    
    posts = data[&#39;db&#39;][0][&#39;data&#39;][&#39;posts&#39;]
    
    for post in posts:
        if post[&#39;status&#39;] == &#39;published&#39;:
            # WriteFreely API Call
            payload = {
                &#39;body&#39;: convert_html_to_markdown(post[&#39;html&#39;]),
                &#39;title&#39;: post[&#39;title&#39;]
            }
            # ... API integration
</code></pre>

<p><strong>My tip</strong>: For small blogs (&lt; 20 posts), manual copying is often faster and cleaner.</p>

<h2 id="custom-css-the-perfect-dark-theme">Custom CSS: The Perfect Dark Theme</h2>

<p>After many iterations, here&#39;s my final WriteFreely dark theme:</p>

<pre><code class="language-css">/* WriteFreely Custom Dark Theme */

/* === SANS-SERIF FONT === */
body, html, * {
    font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, system-ui, sans-serif !important;
}

/* === ANTHRACITE DESIGN === */
body, html {
    background: #2e2e2e !important;
    color: #f0f0f0 !important;
}

/* === COLORED HEADING HIERARCHY === */
body h1 { color: #ffffff !important; font-size: 1.8rem !important; }
body h2 { color: #7db3f3 !important; font-size: 1.5rem !important; }
body h3 { color: #98d982 !important; font-size: 1.3rem !important; }
body h4 { color: #f9cc81 !important; font-size: 1.2rem !important; }

/* === LINKS === */
body a { color: #7db3f3 !important; }
body a:hover { color: #98d982 !important; }

/* === CODE BOXES (This was a battle!) === */
body pre {
    background: #1e1e1e !important;
    color: #f0f0f0 !important;
    border: 1px solid #444 !important;
    border-radius: 8px !important;
    padding: 1rem !important;
}

body code {
    background: #1e1e1e !important;
    color: #f0f0f0 !important;
    border: 1px solid #444 !important;
    border-radius: 8px !important;
    padding: 0.2rem 0.4rem !important;
}

/* === IMAGES WITH ROUNDED CORNERS === */
body img {
    border-radius: 12px !important;
    max-width: 100% !important;
}

/* === REMOVE FOOTER === */
body footer { display: none !important; }
</code></pre>

<h3 id="html-shortcuts-for-beautiful-images">HTML Shortcuts for Beautiful Images</h3>

<p>For consistent image presentation, I use these HTML shortcuts:</p>

<pre><code class="language-html">&lt;!-- Standard image with shadow --&gt;
&lt;img src=&#34;https://cdn.example.com/images/image.png&#34; alt=&#34;Description&#34; 
     style=&#34;border-radius: 12px !important; 
            box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; 
            margin: 2rem auto !important; 
            display: block !important;&#34;&gt;

&lt;!-- Image with caption --&gt;
&lt;figure style=&#34;text-align: center; margin: 2rem auto;&#34;&gt;
    &lt;img src=&#34;https://cdn.example.com/images/image.png&#34; alt=&#34;Description&#34;
         style=&#34;border-radius: 12px !important; 
                box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important;&#34;&gt;
    &lt;figcaption style=&#34;color: #8b949e; font-style: italic; margin-top: 1rem;&#34;&gt;
        Caption here
    &lt;/figcaption&gt;
&lt;/figure&gt;
</code></pre>

<h2 id="activitypub-automatically-in-the-fediverse">ActivityPub: Automatically in the Fediverse</h2>

<p>The best thing about WriteFreely: <strong>It just works!</strong></p>
<ul><li><strong>My blog</strong>: <code><a href="https://blog.klein.ruhr/@/username@blog.example.com" class="u-url mention">@<span>username@blog.example.com</span></a></code></li>
<li><strong>Followers</strong>: Automatically via Mastodon, Pleroma, etc.</li>
<li><strong>Posts</strong>: Automatically federated to the Fediverse</li>
<li><strong>Interaction</strong>: Comments via ActivityPub possible</li></ul>

<h3 id="testing-federation">Testing Federation</h3>

<pre><code class="language-bash"># Test WebFinger
curl https://blog.example.com/.well-known/webfinger?resource=acct:username@blog.example.com

# ActivityPub profile
curl -H &#34;Accept: application/activity+json&#34; https://blog.example.com/@username
</code></pre>

<h2 id="troubleshooting-common-problems">Troubleshooting: Common Problems</h2>

<h3 id="problem-1-code-boxes-stay-white">Problem 1: Code boxes stay white</h3>

<p><strong>Solution</strong>: Edit WriteFreely&#39;s <code>write.css</code> directly:</p>

<pre><code class="language-bash">cd /opt/writefreely/static/css
sudo nano write.css

# Search for: background:#f8f8f8
# Replace with: background:#1e1e1e
</code></pre>

<h3 id="problem-2-css-not-applied">Problem 2: CSS not applied</h3>

<p><strong>Solution</strong>: Aggressively clear browser cache, test different browsers.</p>

<h3 id="problem-3-activitypub-doesn-t-work">Problem 3: ActivityPub doesn&#39;t work</h3>

<p><strong>Solution</strong>:
– <code>federation = true</code> in config.ini
– <code>host = https://your-real-domain.com</code> (not localhost!)
– Reverse proxy configured correctly</p>

<h2 id="webspace-for-images">Webspace for Images</h2>

<p>Separate container for static assets:</p>

<pre><code class="language-yaml"># docker-compose.yml for webspace
services:
  webspace:
    image: httpd:latest
    volumes:
      - ./webspace:/usr/local/apache2/htdocs
    labels:
      - &#34;traefik.http.routers.webspace.rule=(Host(`cdn.example.com`))&#34;
      # ... Additional labels
</code></pre>

<h2 id="conclusion-is-the-switch-worth-it">Conclusion: Is the Switch Worth It?</h2>

<p><strong>Definitely yes!</strong> After migration:</p>

<p>✅ <strong>Much faster</strong> – Page loads lightning fast
✅ <strong>Less maintenance</strong> – One binary, no updates, no Node.js
✅ <strong>Fediverse integration</strong> – My posts automatically reach more people
✅ <strong>Focus on writing</strong> – No distractions from complex themes/plugins
✅ <strong>Resource efficient</strong> – Runs smoothly on small LXC</p>

<h2 id="resources">Resources</h2>
<ul><li><strong>WriteFreely</strong>: <a href="https://writefreely.org">https://writefreely.org</a></li>
<li><strong>ActivityPub Plugin (WordPress)</strong>: <a href="https://wordpress.org/plugins/activitypub/">https://wordpress.org/plugins/activitypub/</a></li>
<li><strong>Fediverse Test</strong>: <a href="https://fediverse.party">https://fediverse.party</a></li></ul>

<h2 id="credits">Credits</h2>
<ul><li><strong>Header Image by <a href="https://unsplash.com/@soy_danielthomas?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Daniel Thomas</a> on <a href="https://unsplash.com/photos/person-using-macbook-pro-on-black-table-gWlBxOAgXgQ?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></strong></li></ul>

<p><em>Migration completed: ✅ Self-hosted, ✅ Fediverse-ready, ✅ Minimal &amp; fast</em></p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/from-ghost-to-writefreely-my-migration-to-the-fediverse</guid>
      <pubDate>Sat, 23 Aug 2025 10:08:17 +0000</pubDate>
    </item>
    <item>
      <title>Self-hosting Matrix in 2025</title>
      <link>https://blog.klein.ruhr/self-hosting-matrix-in-2025</link>
      <description>&lt;![CDATA[&lt;img src=&#34;https://data.klein.ruhr/images/matrixselfhosting.webp&#34; alt=&#34;Beschreibung&#34; &#xA;     style=&#34;border-radius: 12px !important; &#xA;            box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; &#xA;            margin: 2rem auto !important; &#xA;            display: block !important;&#34;  After a longer hiatus, I decided yesterday to tackle setting up my own Matrix server again. The last time I tried this, it was quite a battle with cryptic error messages, Docker permission problems, and federation headaches. This time should be different - and indeed: A lot has changed!&#xA;&#xA;TL;DR: It finally works smoothlyThe key takeaways upfront:&#xA;&#xA;✅ Docker installation runs cleanly through&#xA;✅ .well-known federation is recognized immediately&#xA;✅ Element X with Sliding Sync works out-of-the-box&#xA;✅ Traefik integration without hickups&#xA;✅ IPv4 &amp; IPv6 federation active from the start&#xA;&#xA;In short: What used to cost hours or days now works in under an hour. But let me explain step by step...&#xA;!--more--&#xA;The technical starting pointFor my setup I use:&#xA;&#xA;Synapse as Matrix homeserver&#xA;PostgreSQL as database&#xA;Traefik as reverse proxy with Let&#39;s Encrypt&#xA;Docker Compose for orchestration&#xA;&#xA;The target architecture:&#xA;&#xA;example.com as Matrix domain (for @username:example.com)&#xA;matrix.example.com as technical server endpoint&#xA;Well-Known delegation for clean separation&#xA;&#xA;What used to be problematicDuring my last Matrix installation attempts a few years ago, these were the usual stumbling blocks:&#xA;&#xA;Database permissions&#xA;&#xA;This was standard:&#xA;psql: FATAL: password authentication failed for user &#34;synapse&#34;&#xA;permission denied for table xyz&#xA;&#xA;Federation debugging&#xA;&#xA;Federation tester always resulted in:&#xA;&#34;CanReachViaHTTPS&#34;: false,&#xA;&#34;DNSCheck&#34;: false&#xA;&#xA;Well-Known endpoints&#xA;&#xA;The .well-known/matrix/client and .well-known/matrix/server configuration was always a trial-and-error process with cryptic error messages.&#xA;&#xA;Element X and Sliding&#xA;&#xA;SyncSliding Sync didn&#39;t even exist yet, and when it did, it came with separate proxy containers and complicated configuration.&#xA;&#xA;The 2025 installation: Surprisingly smooth&#xA;&#xA;1. Docker Compose setup&#xA;&#xA;My compose.yml has become refreshingly compact:&#xA;&#xA;services:&#xA;  synapse:&#xA;    image: matrixdotorg/synapse:latest&#xA;    restart: unless-stopped&#xA;    environment:&#xA;      SYNAPSESERVERNAME: example.com&#xA;      SYNAPSEREPORTSTATS: &#34;no&#34;&#xA;    volumes:&#xA;      ./data:/data&#xA;    labels:&#xA;      &#34;traefik.enable=true&#34;&#xA;      &#34;traefik.docker.network=proxy&#34;&#xA;      &#xA;      # Client API (über websecure/443 mit CrowdSec-Schutz)&#xA;      &#34;traefik.http.routers.matrix-client.entrypoints=websecure&#34;&#xA;      &#34;traefik.http.routers.matrix-client.rule=(Host(matrix.example.com))&#34;&#xA;      &#34;traefik.http.routers.matrix-client.tls=true&#34;&#xA;      &#34;traefik.http.routers.matrix-client.tls.certresolver=httpresolver&#34;&#xA;      &#34;traefik.http.routers.matrix-client.service=matrix-client&#34;&#xA;      &#34;traefik.http.routers.matrix-client.middlewares=default@file&#34;&#xA;      &#34;traefik.http.services.matrix-client.loadbalancer.server.port=8008&#34;&#xA;&#xA;      # Federation API (über Port 443, OHNE CrowdSec!)&#xA;      &#34;traefik.http.routers.matrix-federation.entrypoints=matrix-federation&#34;&#xA;      &#34;traefik.http.routers.matrix-federation.rule=(Host(matrix.example.com))&#34;&#xA;      &#34;traefik.http.routers.matrix-federation.tls=true&#34;&#xA;      &#34;traefik.http.routers.matrix-federation.tls.certresolver=httpresolver&#34;&#xA;      &#34;traefik.http.routers.matrix-federation.service=matrix-federation&#34;&#xA;      &#34;traefik.http.services.matrix-federation.loadbalancer.server.port=8008&#34;&#xA;      &#xA;  postgres:&#xA;    image: postgres:15&#xA;    restart: unless-stopped&#xA;    environment:&#xA;      POSTGRESDB: synapse&#xA;      POSTGRESUSER: synapse&#xA;      POSTGRESPASSWORD: securerandompassword&#xA;&#xA;That&#39;s it. No complicated volume mounts, no permission fixes, no custom Dockerfiles.&#xA;&#xA;2. Homeserver configuration&#xA;&#xA;The homeserver.yaml has become significantly more user-friendly. The most important changes for 2025:&#xA;&#xA;Element X Support eingebaut&#xA;experimentalfeatures:&#xA;  msc3575enabled: true&#xA;&#xA;unstablefeatures:&#xA;  org.matrix.simplifiedmsc3575: true&#xA;&#xA;Public Base URL für saubere Links  &#xA;publicbaseurl: &#34;https://matrix.example.com&#34;&#xA;&#xA;Well-Known Federation&#xA;servername: &#34;example.com&#34;&#xA;&#xA;Federation über Port 8008&#xA;listeners:&#xA;  port: 8008&#xA;    tls: false&#xA;    type: http&#xA;    xforwarded: true&#xA;    resources:&#xA;      names: [client, federation]&#xA;        compress: false&#xA;&#xA;That&#39;s it! No separate Sliding Sync proxy, no complicated MSC feature activation.&#xA;&#xA;3. Well-Known delegation&#xA;&#xA;The .well-known/matrix/client on example.com:&#xA;&#xA;{&#xA;  &#34;m.homeserver&#34;: {&#xA;    &#34;baseurl&#34;: &#34;https://matrix.example.com&#34;&#xA;  },&#xA;  &#34;m.identityserver&#34;: {&#xA;    &#34;base_url&#34;: &#34;https://vector.im&#34;&#xA;  }&#xA;}&#xA;&#xA;And .well-known/matrix/server:&#xA;&#xA;{&#xA;  &#34;m.server&#34;: &#34;matrix.example.com:443&#34;&#xA;}&#xA;&#xA;Result: Federation tester shows green immediately!&#xA;&#xA;Element X: The game changer&#xA;&#xA;The biggest difference from before is Element X. Installation was trivial:&#xA;&#xA;Install app&#xA;Enter server: example.com&#xA;Login: @username:example.com&#xA;Sliding Sync works automatically&#xA;&#xA;No separate proxy containers, no experimental API endpoints, no beta features. It just works.&#xA;&#xA;The performance is noticeably better than Element Classic:&#xA;&#xA;Instant synchronization between devices&#xA;Rooms load significantly faster&#xA;Offline support is more robust&#xA;&#xA;Federation: Finally hassle-free&#xA;&#xA;Everything green, IPv4 and IPv6 work, federation runs immediately.&#xA;&#xA;Performance and resource consumption&#xA;&#xA;The current setup is surprisingly efficient:&#xA;&#xA;CONTAINER           CPU %     MEM USAGE / LIMIT     MEM %&#xA;matrix-synapse-1    0.52%     222.5MiB / 23.41GiB   0.93%&#xA;matrix-postgres-1   0.10%     117.9MiB / 23.41GiB   0.49%&#xA;&#xA;Absolutely fine for a single-user server - under 1% CPU load and less than 350MB RAM total. The PostgreSQL integration runs stable, no memory leaks or performance issues.&#xA;&#xA;What I learned&#xA;&#xA;1. The Matrix ecosystem has matured&#xA;&#xA;The development of recent years shows: Matrix has evolved from an interesting experiment to a production-ready platform. Installation has become significantly more user-friendly.&#xA;&#xA;2. Element X makes the difference&#xA;&#xA;Sliding Sync isn&#39;t just a technical feature - it fundamentally changes the user experience. Matrix finally feels as responsive as modern messengers.&#xA;&#xA;3. Well-Known delegation is brilliant&#xA;&#xA;The ability to use @username:example.com as Matrix ID while running the technical server on matrix.example.com is perfect for branding and flexibility.&#xA;&#xA;4. Docker Compose is completely sufficient&#xA;&#xA;No Kubernetes, no complex orchestration tools needed. Docker Compose with Traefik is absolutely adequate for self-hosting setups.&#xA;&#xA;Lessons learned and tips&#xA;&#xA;Do&#39;s ✅&#xA;&#xA;Use PostgreSQL instead of SQLite - even for single-user setups&#xA;Enable Sliding Sync directly in Synapse instead of separate proxies&#xA;Use Well-Known delegation for clean domain separation&#xA;Test federation immediately with the Matrix Federation Tester&#xA;&#xA;Don&#39;ts ❌&#xA;&#xA;No complex identity servers for private setups&#xA;No Element Web if you only use native apps&#xA;No excessive resource limits - Matrix has become more efficient&#xA;Don&#39;t optimize too early - the defaults work well&#xA;&#xA;Conclusion: Matrix is finally self-hosting-ready&#xA;&#xA;After years of &#34;almost, but not quite&#34; experiences, I can say: Matrix self-hosting is finally seamlessly possible in 2025.&#xA;&#xA;Development has reached the point where installation is no longer the most difficult part. Instead, you can focus on what Matrix is designed for: decentralized, secure communication without vendor lock-in.&#xA;&#xA;Who want to reach me directly and encrypted? @matthias:klein.ruhr 🔐]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/matrixselfhosting.webp" alt="Beschreibung" style="border-radius: 12px !important; 
            box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; 
            margin: 2rem auto !important; 
            display: block !important;">
After a longer hiatus, I decided yesterday to tackle setting up my own Matrix server again. The last time I tried this, it was quite a battle with cryptic error messages, Docker permission problems, and federation headaches. This time should be different – and indeed: <strong>A lot</strong> has changed!</p>

<h2 id="tl-dr-it-finally-works-smoothlythe-key-takeaways-upfront">TL;DR: It finally works smoothlyThe key takeaways upfront:</h2>
<ul><li>✅ Docker installation runs cleanly through</li>
<li>✅ <code>.well-known</code> federation is recognized immediately</li>
<li>✅ Element X with Sliding Sync works out-of-the-box</li>
<li>✅ Traefik integration without hickups</li>
<li>✅ IPv4 &amp; IPv6 federation active from the start</li></ul>

<p>In short: What used to cost hours or days now works in under an hour. But let me explain step by step...
</p>

<h2 id="the-technical-starting-pointfor-my-setup-i-use">The technical starting pointFor my setup I use:</h2>
<ul><li><strong>Synapse</strong> as Matrix homeserver</li>
<li><strong>PostgreSQL</strong> as database</li>
<li><strong>Traefik</strong> as reverse proxy with Let&#39;s Encrypt</li>
<li><strong>Docker Compose</strong> for orchestration</li></ul>

<p>The target architecture:</p>
<ul><li><code>example.com</code> as Matrix domain (for @username:example.com)</li>
<li><code>matrix.example.com</code> as technical server endpoint</li>
<li>Well-Known delegation for clean separation</li></ul>

<h2 id="what-used-to-be-problematicduring-my-last-matrix-installation-attempts-a-few-years-ago-these-were-the-usual-stumbling-blocks">What used to be problematicDuring my last Matrix installation attempts a few years ago, these were the usual stumbling blocks:</h2>

<h3 id="database-permissions">Database permissions</h3>

<pre><code># This was standard:
psql: FATAL: password authentication failed for user &#34;synapse&#34;
permission denied for table xyz
</code></pre>

<h3 id="federation-debugging">Federation debugging</h3>

<pre><code># Federation tester always resulted in:
&#34;CanReachViaHTTPS&#34;: false,
&#34;DNSCheck&#34;: false
</code></pre>

<h3 id="well-known-endpoints">Well-Known endpoints</h3>

<p>The <code>.well-known/matrix/client</code> and <code>.well-known/matrix/server</code> configuration was always a trial-and-error process with cryptic error messages.</p>

<h3 id="element-x-and-sliding">Element X and Sliding</h3>

<p>SyncSliding Sync didn&#39;t even exist yet, and when it did, it came with separate proxy containers and complicated configuration.</p>

<h2 id="the-2025-installation-surprisingly-smooth">The 2025 installation: Surprisingly smooth</h2>

<h3 id="1-docker-compose-setup">1. Docker Compose setup</h3>

<p>My <code>compose.yml</code> has become refreshingly compact:</p>

<pre><code>services:
  synapse:
    image: matrixdotorg/synapse:latest
    restart: unless-stopped
    environment:
      SYNAPSE_SERVER_NAME: example.com
      SYNAPSE_REPORT_STATS: &#34;no&#34;
    volumes:
      - ./data:/data
    labels:
      - &#34;traefik.enable=true&#34;
      - &#34;traefik.docker.network=proxy&#34;
      
      # Client API (über websecure/443 mit CrowdSec-Schutz)
      - &#34;traefik.http.routers.matrix-client.entrypoints=websecure&#34;
      - &#34;traefik.http.routers.matrix-client.rule=(Host(`matrix.example.com`))&#34;
      - &#34;traefik.http.routers.matrix-client.tls=true&#34;
      - &#34;traefik.http.routers.matrix-client.tls.certresolver=http_resolver&#34;
      - &#34;traefik.http.routers.matrix-client.service=matrix-client&#34;
      - &#34;traefik.http.routers.matrix-client.middlewares=default@file&#34;
      - &#34;traefik.http.services.matrix-client.loadbalancer.server.port=8008&#34;

      # Federation API (über Port 443, OHNE CrowdSec!)
      - &#34;traefik.http.routers.matrix-federation.entrypoints=matrix-federation&#34;
      - &#34;traefik.http.routers.matrix-federation.rule=(Host(`matrix.example.com`))&#34;
      - &#34;traefik.http.routers.matrix-federation.tls=true&#34;
      - &#34;traefik.http.routers.matrix-federation.tls.certresolver=http_resolver&#34;
      - &#34;traefik.http.routers.matrix-federation.service=matrix-federation&#34;
      - &#34;traefik.http.services.matrix-federation.loadbalancer.server.port=8008&#34;
      
  postgres:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_DB: synapse
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: secure_random_password
</code></pre>

<p>That&#39;s it. No complicated volume mounts, no permission fixes, no custom Dockerfiles.</p>

<h3 id="2-homeserver-configuration">2. Homeserver configuration</h3>

<p>The <code>homeserver.yaml</code> has become significantly more user-friendly. The most important changes for 2025:</p>

<pre><code># Element X Support eingebaut
experimental_features:
  msc3575_enabled: true

unstable_features:
  org.matrix.simplified_msc3575: true

# Public Base URL für saubere Links  
public_baseurl: &#34;https://matrix.example.com&#34;

# Well-Known Federation
server_name: &#34;example.com&#34;

# Federation über Port 8008
listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    resources:
      - names: [client, federation]
        compress: false
</code></pre>

<p><strong>That&#39;s it!</strong> No separate Sliding Sync proxy, no complicated MSC feature activation.</p>

<h3 id="3-well-known-delegation">3. Well-Known delegation</h3>

<p>The <code>.well-known/matrix/client</code> on <code>example.com</code>:</p>

<pre><code>{
  &#34;m.homeserver&#34;: {
    &#34;base_url&#34;: &#34;https://matrix.example.com&#34;
  },
  &#34;m.identity_server&#34;: {
    &#34;base_url&#34;: &#34;https://vector.im&#34;
  }
}
</code></pre>

<p>And <code>.well-known/matrix/server</code>:</p>

<pre><code>{
  &#34;m.server&#34;: &#34;matrix.example.com:443&#34;
}
</code></pre>

<p><strong>Result:</strong> Federation tester shows green immediately!</p>

<h2 id="element-x-the-game-changer">Element X: The game changer</h2>

<p>The biggest difference from before is Element X. Installation was trivial:</p>
<ol><li>Install app</li>
<li>Enter server: <code>example.com</code></li>
<li>Login: <code>@username:example.com</code></li>
<li>Sliding Sync works automatically</li></ol>

<p>No separate proxy containers, no experimental API endpoints, no beta features. It just works.</p>

<p>The performance is noticeably better than Element Classic:</p>
<ul><li>Instant synchronization between devices</li>
<li>Rooms load significantly faster</li>
<li>Offline support is more robust</li></ul>

<h2 id="federation-finally-hassle-free">Federation: Finally hassle-free</h2>

<p>Everything green, IPv4 and IPv6 work, federation runs immediately.</p>

<p><img src="https://data.klein.ruhr/images/federation-test-result.png" alt=""></p>

<h2 id="performance-and-resource-consumption">Performance and resource consumption</h2>

<p>The current setup is surprisingly efficient:</p>

<pre><code>CONTAINER           CPU %     MEM USAGE / LIMIT     MEM %
matrix-synapse-1    0.52%     222.5MiB / 23.41GiB   0.93%
matrix-postgres-1   0.10%     117.9MiB / 23.41GiB   0.49%
</code></pre>

<p>Absolutely fine for a single-user server – under 1% CPU load and less than 350MB RAM total. The PostgreSQL integration runs stable, no memory leaks or performance issues.</p>

<h2 id="what-i-learned">What I learned</h2>

<h3 id="1-the-matrix-ecosystem-has-matured">1. The Matrix ecosystem has matured</h3>

<p>The development of recent years shows: Matrix has evolved from an interesting experiment to a production-ready platform. Installation has become significantly more user-friendly.</p>

<h3 id="2-element-x-makes-the-difference">2. Element X makes the difference</h3>

<p>Sliding Sync isn&#39;t just a technical feature – it fundamentally changes the user experience. Matrix finally feels as responsive as modern messengers.</p>

<h3 id="3-well-known-delegation-is-brilliant">3. Well-Known delegation is brilliant</h3>

<p>The ability to use <code>@username:example.com</code> as Matrix ID while running the technical server on <code>matrix.example.com</code> is perfect for branding and flexibility.</p>

<h3 id="4-docker-compose-is-completely-sufficient">4. Docker Compose is completely sufficient</h3>

<p>No Kubernetes, no complex orchestration tools needed. Docker Compose with Traefik is absolutely adequate for self-hosting setups.</p>

<h2 id="lessons-learned-and-tips">Lessons learned and tips</h2>

<h3 id="do-s">Do&#39;s ✅</h3>
<ul><li><strong>Use PostgreSQL</strong> instead of SQLite – even for single-user setups</li>
<li><strong>Enable Sliding Sync</strong> directly in Synapse instead of separate proxies</li>
<li><strong>Use Well-Known delegation</strong> for clean domain separation</li>
<li><strong>Test federation immediately</strong> with the Matrix Federation Tester</li></ul>

<h3 id="don-ts">Don&#39;ts ❌</h3>
<ul><li><strong>No complex identity servers</strong> for private setups</li>
<li><strong>No Element Web</strong> if you only use native apps</li>
<li><strong>No excessive resource limits</strong> – Matrix has become more efficient</li>
<li><strong>Don&#39;t optimize too early</strong> – the defaults work well</li></ul>

<h2 id="conclusion-matrix-is-finally-self-hosting-ready">Conclusion: Matrix is finally self-hosting-ready</h2>

<p>After years of “almost, but not quite” experiences, I can say: <strong>Matrix self-hosting is finally seamlessly possible in 2025.</strong></p>

<p>Development has reached the point where installation is no longer the most difficult part. Instead, you can focus on what Matrix is designed for: decentralized, secure communication without vendor lock-in.</p>

<p>Who want to reach me directly and encrypted? <a href="https://matrix.to/#/@matthias:klein.ruhr"><strong>@matthias:klein.ruhr</strong></a> 🔐</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/self-hosting-matrix-in-2025</guid>
      <pubDate>Sun, 10 Aug 2025 17:15:57 +0000</pubDate>
    </item>
    <item>
      <title>GoToSocial - ready for Prime Time?</title>
      <link>https://blog.klein.ruhr/gotosocial-ready-for-prime-time</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/gotosocialprimetime.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;Short answer: Hell yes.&#xA;&#xA;After completing the full migration and running in production for weeks, I can confidently say GoToSocial is ready for serious use.&#xA;&#xA;Perfect For: ✅&#xA;&#xA;Self-hosters with limited resources (VPS with 1-2GB RAM? No problem!)&#xA;Single-user or small family instances (works great up to ~50 users based on community reports)&#xA;Homelab enthusiasts who value efficiency and simplicity&#xA;Privacy-conscious users who want full control over their data&#xA;Developers who appreciate clean, well-documented APIs&#xA;&#xA;Think Twice If: ⚠️&#xA;&#xA;Large communities (100+ users - not extensively tested yet)&#xA;Power users who rely heavily on advanced Mastodon features&#xA;Mobile-first users who need perfect app compatibility&#xA;Non-technical admins who prefer GUI-based administration&#xA;!--more--&#xA;Resource Requirements: The Reality Check&#xA;&#xA;Minimum viable setup:&#xA;VPS: 1GB RAM, 1 CPU core, 20GB storage&#xA;Cost: ~$5-10/month&#xA;&#xA;My production setup:&#xA;Server: Proxmox LXC container&#xA;Resources: 2GB RAM, 2 CPU cores, 50GB storage  &#xA;Actual usage: 426MB RAM average, 15% CPU peak&#xA;Cost: $0 (runs on existing homelab hardware)&#xA;&#xA;Scaling considerations:&#xA;Single-user: 512MB RAM sufficient&#xA;5-10 users: 1GB RAM recommended&#xA;10-50 users: 2GB RAM should handle it&#xA;Database grows ~1-2GB per month with active use&#xA;&#xA;Why Even Consider Switching?&#xA;&#xA;After months of running a self-hosted Mastodon instance in my homelab, I was getting increasingly frustrated. Not with the Fediverse concept - that&#39;s brilliant - but with Mastodon&#39;s resource appetite.&#xA;&#xA;The Starting Point:&#xA;&#xA;Mastodon Docker stack: 4-5 GB RAM consumption&#xA;Storage growth: ~100 GB with 7-day media retention&#xA;Performance: Noticeable delays in timeline updates&#xA;Maintenance: Regular cleanup scripts required to keep things manageable&#xA;&#xA;When you&#39;re running 60+ Docker stacks in your homelab like I am, resource efficiency isn&#39;t just nice-to-have - it&#39;s essential. GoToSocial promised the same Fediverse functionality at a fraction of the resource cost.&#xA;&#xA;Technical Stack Context:&#xA;&#xA;My homelab setup&#xA;Proxmox Cluster: 3 nodes&#xA;Total Containers: 60+ Docker stacks&#xA;Mastodon Dependencies:&#xA;  PostgreSQL (shared instance)&#xA;  Redis (shared instance) &#xA;  Elasticsearch (optional, but resource-hungry)&#xA;  Sidekiq workers (multiple processes)&#xA;  Web server + streaming API&#xA;&#xA;The complexity was getting out of hand.&#xA;&#xA;The Migration Process: Technical Deep-Dive&#xA;&#xA;Testing Phase&#xA;&#xA;Before committing fully, I ran GoToSocial parallel to Mastodon for four weeks. This gave me real-world data without burning bridges.&#xA;&#xA;compose.yml - GoToSocial setup&#xA;services:&#xA;  gotosocial:&#xA;    image: superseriousbusiness/gotosocial:latest&#xA;    containername: gotosocial&#xA;    restart: unless-stopped&#xA;    environment:&#xA;      GTSHOST: your-domain.com&#xA;      GTSDBTYPE: postgres&#xA;      GTSDBADDRESS: postgres:5432&#xA;      GTSDBDATABASE: gotosocial&#xA;      GTSDBUSER: gotosocial&#xA;      GTSDBPASSWORD: ${GTSDBPASSWORD}&#xA;      GTSACCOUNTSREGISTRATIONOPEN: false&#xA;      GTSACCOUNTSAPPROVALREQUIRED: true&#xA;      GTSMEDIAREMOTECACHEDAYS: 7&#xA;      GTSSTORAGELOCALBASEPATH: /gotosocial/storage&#xA;    volumes:&#xA;      ./data:/gotosocial/storage&#xA;    networks:&#xA;      traefikdefault&#xA;      gotosocialdb&#xA;    labels:&#xA;      &#34;traefik.enable=true&#34;&#xA;      &#34;traefik.http.routers.gotosocial.rule=Host(\your-domain.com\)&#34;&#xA;      &#34;traefik.http.routers.gotosocial.tls.certresolver=letsencrypt&#34;&#xA;    dependson:&#xA;      postgres&#xA;&#xA;  postgres:&#xA;    image: postgres:15-alpine&#xA;    containername: gotosocialdb&#xA;    restart: unless-stopped&#xA;    environment:&#xA;      POSTGRESDB: gotosocial&#xA;      POSTGRESUSER: gotosocial&#xA;      POSTGRESPASSWORD: ${GTSDBPASSWORD}&#xA;    volumes:&#xA;      ./postgres:/var/lib/postgresql/data&#xA;    networks:&#xA;      gotosocialdb&#xA;&#xA;Key Configuration Decisions:&#xA;&#xA;Separate Database: Not sharing with Mastodon to avoid conflicts&#xA;7-Day Media Retention: Balancing storage vs. content availability&#xA;Traefik Integration: Seamless SSL termination and routing&#xA;Bind Mounts: Easy backup/restore without Docker volume complexity&#xA;&#xA;Account Migration: The Technical Reality&#xA;&#xA;The actual migration process was surprisingly smooth, but had some gotchas:&#xA;&#xA;1\. Mastodon Export&#xA;&#xA;Export process (takes 24-48 hours for large instances)&#xA;Mastodon Admin → Settings → Import/Export → Request Archive&#xA;Results in: archive-[timestamp].tar.gz&#xA;&#xA;2\. Post Import with slurp&#xA;&#xA;Install slurp&#xA;git clone https://github.com/superseriousbusiness/gotosocial-slurp&#xA;cd gotosocial-slurp&#xA;go build .&#xA;&#xA;Import 380 posts from Mastodon archive&#xA;./slurp import \&#xA;  --gts-endpoint https://your-domain.com \&#xA;  --gts-token YOURGTSTOKEN \&#xA;  --mastodon-archive archive-20241215.tar.gz \&#xA;  --dry-run false&#xA;&#xA;Results: Perfect import with media, metadata, and timestamps preserved&#xA;&#xA;3\. Account Redirect Setup&#xA;&#xA;In Mastodon rails console&#xA;rails console&#xA;account = Account.findby(username: &#39;yourusername&#39;)&#xA;account.update(movedtoaccountid: nil) # Clear any previous moves&#xA;Then use Mastodon UI: Settings → Account → Move to different account&#xA;&#xA;Migration Casualties:&#xA;&#xA;Lost ~50 followers (12% dropout rate)&#xA;Likely causes: inactive accounts, app compatibility issues, users who don&#39;t follow redirects&#xA;Lesson: Quality over quantity - the remaining followers are actually engaged&#xA;&#xA;Performance Benchmarks: Numbers Don&#39;t Lie&#xA;&#xA;Memory Usage Deep-Dive&#xA;&#xA;Monitoring setup with docker stats&#xA;docker stats --format &#34;table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}&#34;&#xA;&#xA;Mastodon (before migration):&#xA;CONTAINER       CPU %    MEM USAGE / LIMIT     MEM %&#xA;mastodonweb    45.2%    2.1GiB / 8GiB        26.25%&#xA;mastodonsidekiq 23.1%   1.8GiB / 8GiB        22.50%&#xA;mastodonstreaming 12.4% 512MiB / 8GiB        6.40%&#xA;redis           8.7%     256MiB / 8GiB        3.20%&#xA;postgres        15.3%    800MiB / 8GiB        10.00%&#xA;elasticsearch   35.8%    1.2GiB / 8GiB        15.00%&#xA;Total: ~6.7GB peak usage&#xA;&#xA;GoToSocial (after migration):&#xA;CONTAINER       CPU %    MEM USAGE / LIMIT     MEM %&#xA;gotosocial      12.3%    419MiB / 8GiB        5.24%&#xA;postgres        3.2%     128MiB / 8GiB        1.60%&#xA;fedifetcher     0.8%     3.4MiB / 8GiB        0.04%&#xA;gts-holmirdas   1.1%     3.7MiB / 8GiB        0.05%&#xA;Total: ~555MB peak usage&#xA;&#xA;RAM Efficiency Gain: 92% reduction (6.7GB → 555MB)&#xA;&#xA;Storage Analysis&#xA;&#xA;Mastodon storage breakdown (after 6 months):&#xA;du -sh /opt/mastodon/&#xA;12G     system/  (cache, mediaattachments)&#xA;45G     public/  (user uploads, cached media)&#xA;43G     postgresdata/  (database)&#xA;Total: ~100GB with aggressive 7-day cleanup&#xA;&#xA;GoToSocial storage (extrapolated to same timeframe):&#xA;du -sh /opt/gotosocial/&#xA;8.2G    storage/  (all media, database)&#xA;26G     postgresdata/  (more efficient schema)&#xA;Total: ~35GB with 7-day remote media retention&#xA;&#xA;Storage Efficiency Gain: 65% reduction&#xA;&#xA;Why GoToSocial is so much leaner:&#xA;&#xA;Single binary: No separate web/worker/streaming processes&#xA;Efficient database schema: Less metadata overhead per post&#xA;Smart media handling: Better deduplication and compression&#xA;No Elasticsearch: Full-text search handled by PostgreSQL&#xA;&#xA;Federation Performance: Quality Over Quantity&#xA;&#xA;One of my biggest concerns was federation reach. Would a single-user GoToSocial instance feel isolated?&#xA;&#xA;The Data After 4 Days:&#xA;&#xA;Federation stats from GoToSocial admin API&#xA;curl -H &#34;Authorization: Bearer $ADMINTOKEN&#34; \&#xA;  https://your-domain.com/api/v1/admin/accounts/stats&#xA;&#xA;{&#xA;  &#34;knowninstances&#34;: 2750,&#xA;  &#34;federatedposts&#34;: 12847,&#xA;  &#34;localposts&#34;: 380,&#xA;  &#34;activeusersweek&#34;: 1&#xA;}&#xA;&#xA;Federation happens FAST:&#xA;&#xA;2,750+ instances discovered in 4 days&#xA;No traditional relays needed&#xA;Content discovery through intelligent RSS integration&#xA;Federation quality actually improved (more relevant content, less noise)&#xA;&#xA;Content Discovery: Building GTS-HolMirDas&#xA;&#xA;Here&#39;s where things got interesting. Small Fediverse instances suffer from the &#34;empty timeline problem&#34; - without many local users or relay connections, your federated timeline stays pretty barren.&#xA;&#xA;The Technical Challenge&#xA;&#xA;Traditional approach: Join relays&#xA;Problems:&#xA;- High resource usage&#xA;- Lots of irrelevant content  &#xA;- Reliability issues&#xA;- Limited control over content quality&#xA;&#xA;My Solution: Intelligent RSS-to-Fediverse Bridge&#xA;&#xA;I built GTS-HolMirDas (German pun meaning &#34;fetch this for me&#34;), an open-source tool that:&#xA;&#xA;Core Architecture:&#xA;&#xA;Simplified architecture overview&#xA;class HolMirDas:&#xA;    def init(self):&#xA;        self.rssfeeds = self.loadfeedconfig()  # 102 RSS feeds&#xA;        self.gtsclient = GoToSocialClient()&#xA;        self.duplicatetracker = DuplicateDetector()&#xA;        self.instancetracker = InstanceTracker()&#xA;    &#xA;    def runcycle(self):&#xA;        &#34;&#34;&#34;Main processing loop - runs every hour&#34;&#34;&#34;&#xA;        for feedurl, config in self.rssfeeds.items():&#xA;            self.processfeed(feedurl, config)&#xA;        &#xA;        self.cleanupoldentries()&#xA;        self.updatefederationstats()&#xA;&#xA;Smart Features:&#xA;&#xA;Cross-platform attribution: Properly credits Mastodon, Misskey, Pleroma authors&#xA;Duplicate detection: Prevents the same post from appearing multiple times&#xA;Instance discovery: Automatically tracks new Fediverse instances&#xA;Rate limiting: Respects both RSS and GoToSocial API limits&#xA;&#xA;Performance Metrics&#xA;&#xA;Real-time stats from my instance:&#xA;📊 GTS-HolMirDas Run Statistics:&#xA;   ⏱️  Runtime: 4:39 minutes per cycle&#xA;   📄 Posts processed: 65 per hour  &#xA;   🌐 Current known instances: 2,697&#xA;   ➕ New instances discovered: +22 per run&#xA;   📡 RSS feeds monitored: 102&#xA;   ⚡ Processing rate: 13.9 posts/minute&#xA;   🔄 Runs every 60 minutes automatically&#xA;&#xA;RSS Feed Sources&#xA;&#xA;feedconfig.yml (excerpt)&#xA;feeds:&#xA;  # Major Mastodon instances&#xA;  url: &#34;https://mastodon.social/@Gargron.rss&#34;&#xA;    platform: &#34;mastodon&#34;&#xA;    instance: &#34;mastodon.social&#34;&#xA;  &#xA;  # Specialized instances  &#xA;  url: &#34;https://fosstodon.org/users/exampleuser.rss&#34;&#xA;    platform: &#34;mastodon&#34;&#xA;    instance: &#34;fosstodon.org&#34;&#xA;    &#xA;  # Other Fediverse platforms&#xA;  url: &#34;https://pixelfed.social/users/photographer.atom&#34;&#xA;    platform: &#34;pixelfed&#34;&#xA;    instance: &#34;pixelfed.social&#34;&#xA;&#xA;The Result: A vibrant federated timeline despite being a single-user instance, with content that&#39;s actually relevant to my interests.&#xA;&#xA;Lessons Learned: The Good, The Bad, The Ugly&#xA;&#xA;What Works Brilliantly ✅&#xA;&#xA;Resource Efficiency is Game-Changing&#xA;&#xA;Single binary vs. multiple processes: Much simpler debugging&#xA;Memory usage stays consistently under 500MB even under load&#xA;CPU usage rarely spikes above 15% during federation heavy-lifting&#xA;Storage growth is predictable and manageable&#xA;&#xA;Federation Just Works™&#xA;&#xA;No manual instance discovery needed&#xA;ActivityPub compatibility is excellent&#xA;Cross-platform federation (Mastodon ↔ Misskey ↔ Pleroma) seamless&#xA;Better federation performance than expected&#xA;&#xA;Operational Simplicity&#xA;&#xA;Mastodon maintenance (weekly):&#xA;docker compose exec web bundle exec rake mastodon:media:removeremote&#xA;docker compose exec web bundle exec rake mastodon:previewcards:removeold&#xA;docker compose exec web bundle exec rake mastodon:feeds:build&#xA;+ manual Elasticsearch index management&#xA;+ Sidekiq queue monitoring&#xA;+ Redis memory management&#xA;&#xA;GoToSocial maintenance (monthly):&#xA;That&#39;s it. Seriously.&#xA;&#xA;The Challenges ⚠️&#xA;&#xA;Client Ecosystem&#xA;&#xA;Mobile Apps: Hit-or-miss compatibility&#xA;&#x9;iOS Tusker: Works perfectly, actively maintained&#xA;&#x9;iOS Ice Cubes: Notification issues, otherwise great&#xA;&#x9;Android Tusky: Mostly works, some UI quirks&#xA;&#x9;Android Fedilab: Best overall experience&#xA;Web Clients: Need self-hosting for optimal experience&#xA;&#xA;Missing Mastodon Features (That You Might Miss)&#xA;&#xA;Advanced search capabilities (no Elasticsearch)&#xA;Trending hashtags/posts&#xA;Some admin tools are more basic&#xA;Limited API endpoints compared to Mastodon&#xA;&#xA;Federation Edge Cases&#xA;&#xA;Occasional federation hiccups I encountered:&#xA;Large instances sometimes lag on post delivery (not GTS fault)&#xA;Some Mastodon instances reject GTS posts (rare, usually config issues)&#xA;Media federation can be slow during high-traffic periods&#xA;&#xA;Mobile Apps: Real-World Testing&#xA;&#xA;iOS Testing Results:&#xA;&#xA;Tusker: 9/10&#xA;&#xA;✅ Full feature support&#xA;✅ Reliable notifications&#xA;✅ Good performance&#xA;❌ Paid app ($3.99)&#xA;&#xA;Ice Cubes: 7/10&#xA;&#xA;✅ Beautiful interface&#xA;✅ Active development&#xA;❌ Notification reliability issues&#xA;❌ Some API compatibility gaps&#xA;&#xA;Self-Hosted Web Clients:&#xA;&#xA;My current setup&#xA;services:&#xA;  phanpy:&#xA;    image: cheeaun/phanpy:latest&#xA;    containername: phanpy&#xA;    environment:&#xA;      DEFAULTINSTANCE: your-domain.com&#xA;    labels:&#xA;      &#34;traefik.http.routers.phanpy.rule=Host(\social.your-domain.com\)&#34;&#xA;  &#xA;  elk:&#xA;    image: elkzone/elk:latest&#xA;    containername: elk&#xA;    environment:&#xA;      DEFAULTSERVER: your-domain.com&#xA;    labels:&#xA;      &#34;traefik.http.routers.elk.rule=Host(\elk.your-domain.com\)&#34;&#xA;&#xA;Client Comparison:&#xA;&#xA;Phanpy: Better features, more Mastodon-like, slightly dated UI&#xA;Elk: Gorgeous interface, modern UX, fewer power-user features&#xA;Official GTS Web UI: Basic but functional, good for admin tasks&#xA;&#xA;Cost Analysis&#xA;&#xA;Comparison with Mastodon hosting costs:&#xA;&#xA;Mastodon requirements:&#xA;&#xA;Minimum: 4GB RAM, 2 cores&#xA;Monthly VPS cost: $20-40&#xA;Additional services needed: Redis, Elasticsearch (optional but recommended)&#xA;&#xA;GoToSocial requirements:&#xA;&#xA;Minimum: 1GB RAM, 1 core&#xA;Monthly VPS cost: $5-10&#xA;Additional services: Just PostgreSQL (can use SQLite for single-user)&#xA;&#xA;Annual savings: $180-360 per year&#xA;&#xA;Backup Strategy: Lessons from Migration&#xA;&#xA;!/bin/bash&#xA;My automated backup script&#xA;DATE=$(date +%Y%m%d%H%M%S)&#xA;BACKUPDIR=&#34;/opt/backups/gotosocial&#34;&#xA;&#xA;Database backup&#xA;docker compose exec -T postgres pgdump -U gotosocial gotosocial   \&#xA;  &#34;$BACKUPDIR/dbbackup$DATE.sql&#34;&#xA;&#xA;Media and storage backup&#xA;tar -czf &#34;$BACKUPDIR/storagebackup$DATE.tar.gz&#34; \&#xA;  /opt/gotosocial/data/&#xA;&#xA;Configuration backup  &#xA;cp compose.yml &#34;$BACKUPDIR/composebackup$DATE.yml&#34;&#xA;&#xA;Keep only last 7 days of backups&#xA;find &#34;$BACKUPDIR&#34; -name &#34;.sql&#34; -mtime +7 -delete&#xA;find &#34;$BACKUPDIR&#34; -name &#34;.tar.gz&#34; -mtime +7 -delete&#xA;&#xA;echo &#34;Backup completed: $DATE&#34;&#xA;&#xA;Recovery tested and verified - I&#39;ve successfully restored from backup during my testing phase. The process is straightforward and well-documented.&#xA;&#xA;The Tools That Make It Work&#xA;&#xA;All the supporting tools I built are open source and production-ready:&#xA;&#xA;GTS-HolMirDas&#xA;&#xA;Repository:https://git.klein.ruhr/matthias/gts-holmirdas&#xA;&#xA;Features:&#xA;&#xA;102 RSS feed sources from diverse Fediverse instances&#xA;Cross-platform attribution (Mastodon, Misskey, Pleroma, Pixelfed)&#xA;Intelligent duplicate detection&#xA;Automatic instance discovery&#xA;Rate limiting and error handling&#xA;Docker-ready deployment&#xA;&#xA;Setup in 5 minutes:&#xA;&#xA;git clone https://git.your-domain.com/username/gts-holmirdas.git&#xA;cd gts-holmirdas&#xA;cp config.example.yml config.yml&#xA;Edit config.yml with your GTS instance details&#xA;docker compose up -d&#xA;&#xA;FediFetcher Integration&#xA;&#xA;Repository: blog.thms.uk/fedifetcher&#xA;&#xA;While not my creation, FediFetcher perfectly complements GoToSocial for thread completion:&#xA;&#xA;compose.yml addition&#xA;services:&#xA;  fedifetcher:&#xA;    image: nanos/fedifetcher:latest&#xA;    containername: fedifetcher&#xA;    environment:&#xA;      HOMEINSTANCE=your-domain.com&#xA;      ACCESSTOKEN=${FEDIFETCHERTOKEN}&#xA;      REPLYINTERVALHOURS=1&#xA;      BACKFILLHOURS=24&#xA;    restart: unless-stopped&#xA;&#xA;What it does:&#xA;&#xA;Fetches missing replies to posts in your timeline&#xA;Completes conversation threads automatically&#xA;Backfills content from instances your server hasn&#39;t seen yet&#xA;Minimal resource usage (3-4MB RAM)&#xA;&#xA;Monitoring and Observability&#xA;&#xA;Prometheus metrics (optional but recommended)&#xA;services:&#xA;  prometheus:&#xA;    image: prom/prometheus:latest&#xA;    volumes:&#xA;      ./prometheus.yml:/etc/prometheus/prometheus.yml&#xA;    command:&#xA;      &#39;--config.file=/etc/prometheus/prometheus.yml&#39;&#xA;      &#39;--storage.tsdb.path=/prometheus&#39;&#xA;      &#39;--web.console.libraries=/etc/prometheus/consolelibraries&#39;&#xA;&#xA;prometheus.yml&#xA;scrapeconfigs:&#xA;  jobname: &#39;gotosocial&#39;&#xA;    staticconfigs:&#xA;      targets: [&#39;gotosocial:8080&#39;]&#xA;    metricspath: &#39;/metrics&#39;&#xA;&#xA;Key metrics to monitor:&#xA;&#xA;Memory usage trends&#xA;Database growth rate&#xA;Federation success rates&#xA;API response times&#xA;Storage utilization&#xA;&#xA;Migration Gotchas and Pro Tips&#xA;&#xA;Things I Wish I&#39;d Known Before Starting&#xA;&#xA;1\. Test Federation Early&#xA;&#xA;Verify federation is working before migrating followers&#xA;curl -H &#34;Accept: application/activity+json&#34; \&#xA;  https://your-domain.com/users/yourusername&#xA;&#xA;Should return ActivityPub JSON, not HTML&#xA;&#xA;2\. Mobile App Testing is Critical  &#xA;Don&#39;t assume your favorite Mastodon app will work perfectly. Test extensively with your actual usage patterns before committing.&#xA;&#xA;3\. Content Discovery Takes Time  &#xA;The federated timeline won&#39;t be instantly populated. Plan for a &#34;quiet period&#34; of 1-2 weeks while content discovery tools build up your federation network.&#xA;&#xA;4\. Backup Before Migration&#xA;&#xA;Create a complete Mastodon backup before starting&#xA;docker compose exec web rake mastodon:backup:create&#xA;This creates a full export in /app/backup/&#xA;&#xA;5\. Domain Considerations  &#xA;If you&#39;re changing domains during migration (like I did), be prepared for some federation hiccups. Some instances cache domain information aggressively.&#xA;&#xA;Performance Tuning Tips&#xA;&#xA;PostgreSQL optimization for GoToSocial&#xA;Add to postgres container environment:&#xA;POSTGRESINITDBARGS: &#34;--data-checksums&#34;&#xA;In postgresql.conf:&#xA;sharedbuffers = 128MB          # For 1GB RAM systems&#xA;effectivecachesize = 512MB    # 3/4 of available RAM&#xA;workmem = 4MB                  # Per query operation&#xA;maintenanceworkmem = 64MB     # For maintenance operations&#xA;GoToSocial-specific optimizations:&#xA;In your .env file:&#xA;GTSCACHEMEMORYTARGET=100MB           # Adjust based on available RAM&#xA;GTSMEDIAREMOTECACHEDAYS=7           # Balance storage vs. availability&#xA;GTSACCOUNTSALLOWCUSTOMCSS=false     # Disable if not needed&#xA;GTSWEBASSETBASEDIR=/gotosocial/web  # Serve assets efficiently&#xA;&#xA;Community Impact and Future Plans&#xA;&#xA;What This Migration Means for the Fediverse&#xA;&#xA;Resource Democracy: By proving that high-quality Fediverse instances can run on minimal resources, we&#39;re making decentralization more accessible to everyone.&#xA;&#xA;Tool Ecosystem: The tools developed for this migration (especially GTS-HolMirDas) are helping other small instance operators solve the same content discovery challenges.&#xA;&#xA;Documentation: This migration provides a real-world data point for others considering similar moves.&#xA;&#xA;Future Development Plans&#xA;&#xA;GTS-HolMirDas Roadmap:&#xA;&#xA;Web-based configuration interface&#xA;Better RSS feed quality scoring&#xA;Integration with more Fediverse platforms (PeerTube, Funkwhale)&#xA;Machine learning-based content relevance filtering&#xA;Multi-instance support for hosting providers&#xA;&#xA;Community Feedback Integration: Based on initial user feedback, the most requested features are:&#xA;&#xA;Easier RSS feed discovery and management&#xA;Better content filtering options&#xA;Integration with existing relay networks&#xA;Automated instance health monitoring&#xA;&#xA;Bottom Line: The Numbers Don&#39;t Lie&#xA;&#xA;Migration Summary:&#xA;&#xA;Timeline: 4 weeks of testing, 2 days for full migration&#xA;Resource savings: 92% RAM reduction, 65% storage reduction&#xA;Follower retention: 88% (better than expected)&#xA;Federation growth: 2,750+ instances in first week&#xA;Operational complexity: Significantly reduced&#xA;Cost savings: $180-360 annually in hosting costs&#xA;Satisfaction level: 9.5/10 (would migrate again)&#xA;&#xA;The Real Kicker: GoToSocial doesn&#39;t just match Mastodon&#39;s functionality at lower resource usage - in many ways, it performs better. The federated timeline is more relevant, the interface is faster, and the operational overhead is practically non-existent.&#xA;&#xA;You should definitely consider GoToSocial if:&#xA;&#xA;You&#39;re running a single-user or small instance&#xA;Resource efficiency matters to you&#xA;You value operational simplicity&#xA;You&#39;re comfortable with slightly less mature tooling&#xA;You want to contribute to Fediverse diversity&#xA;&#xA;Stick with Mastodon if:&#xA;&#xA;You&#39;re running a large community (100+ users)&#xA;You need enterprise-grade admin tools&#xA;Your users depend on specific Mastodon features&#xA;You have abundant server resources&#xA;Stability trumps efficiency for your use case&#xA;&#xA;My recommendation: Set up a test GoToSocial instance and run it parallel to your existing setup for a month. The resource cost is minimal, and you&#39;ll get real-world data for your specific use case.&#xA;&#xA;The Fediverse is strongest when it&#39;s diverse - different platforms solving different problems for different communities. GoToSocial has earned its place in that ecosystem.&#xA;&#xA;---&#xA;&#xA;Resources and Links&#xA;&#xA;Essential Tools&#xA;&#xA;GoToSocial:https://gotosocial.org&#xA;GTS-HolMirDas:https://git.klein.ruhr/matthias/gts-holmirdas&#xA;FediFetcher:https://blog.thms.uk/fedifetcher&#xA;Slurp (Post Migration):https://github.com/VyrCossont/slurp&#xA;&#xA;Mobile Apps That Work Well&#xA;&#xA;iOS Tusker: App Store (paid, highly recommended)&#xA;iOS Ice Cubes: App Store (free, some quirks)&#xA;Android Fedilab: F-Droid/Play Store (best Android option)&#xA;Android Tusky: F-Droid/Play Store (decent alternative)&#xA;&#xA;Web Clients (Self-Hosted)&#xA;&#xA;Phanpy:https://github.com/cheeaun/phanpy&#xA;Elk:https://github.com/elk-zone/elk&#xA;&#xA;Community&#xA;&#xA;GoToSocial Documentation:https://docs.gotosocial.org&#xA;Matrix Support: #gotosocial:superseriousbusiness.org&#xA;GitHub Issues:https://github.com/superseriousbusiness/gotosocial&#xA;&#xA;---&#xA;&#xA;Questions? Feedback? Find me on the Fediverse at @matthias@me.klein.ruhr - I&#39;m always happy to discuss self-hosting, the Fediverse, and efficient technology solutions!&#xA;&#xA;If this post helped you with your own migration, consider starring the GTS-HolMirDas repository or contributing to the GoToSocial project. The Fediverse grows stronger with every successful self-hosted instance.]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/gotosocialprimetime.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
Short answer: Hell yes.</p>

<p>After completing the full migration and running in production for weeks, I can confidently say GoToSocial is ready for serious use.</p>

<p><strong>Perfect For:</strong> ✅</p>
<ul><li>Self-hosters with limited resources (VPS with 1-2GB RAM? No problem!)</li>
<li>Single-user or small family instances (works great up to ~50 users based on community reports)</li>
<li>Homelab enthusiasts who value efficiency and simplicity</li>
<li>Privacy-conscious users who want full control over their data</li>
<li>Developers who appreciate clean, well-documented APIs</li></ul>

<p><strong>Think Twice If:</strong> ⚠️</p>
<ul><li>Large communities (100+ users – not extensively tested yet)</li>
<li>Power users who rely heavily on advanced Mastodon features</li>
<li>Mobile-first users who need perfect app compatibility</li>

<li><p>Non-technical admins who prefer GUI-based administration
</p>

<h2 id="resource-requirements-the-reality-check">Resource Requirements: The Reality Check</h2></li></ul>

<pre><code class="language-yaml"># Minimum viable setup:
VPS: 1GB RAM, 1 CPU core, 20GB storage
Cost: ~$5-10/month

# My production setup:
Server: Proxmox LXC container
Resources: 2GB RAM, 2 CPU cores, 50GB storage  
Actual usage: 426MB RAM average, 15% CPU peak
Cost: $0 (runs on existing homelab hardware)

# Scaling considerations:
- Single-user: 512MB RAM sufficient
- 5-10 users: 1GB RAM recommended
- 10-50 users: 2GB RAM should handle it
- Database grows ~1-2GB per month with active use
</code></pre>

<h2 id="why-even-consider-switching">Why Even Consider Switching?</h2>

<p>After months of running a self-hosted Mastodon instance in my homelab, I was getting increasingly frustrated. Not with the Fediverse concept – that&#39;s brilliant – but with Mastodon&#39;s resource appetite.</p>

<p><strong>The Starting Point:</strong></p>
<ul><li>Mastodon Docker stack: 4-5 GB RAM consumption</li>
<li>Storage growth: ~100 GB with 7-day media retention</li>
<li>Performance: Noticeable delays in timeline updates</li>
<li>Maintenance: Regular cleanup scripts required to keep things manageable</li></ul>

<p>When you&#39;re running 60+ Docker stacks in your homelab like I am, resource efficiency isn&#39;t just nice-to-have – it&#39;s essential. GoToSocial promised the same Fediverse functionality at a fraction of the resource cost.</p>

<p><strong>Technical Stack Context:</strong></p>

<pre><code class="language-yaml"># My homelab setup
Proxmox Cluster: 3 nodes
Total Containers: 60+ Docker stacks
Mastodon Dependencies:
  - PostgreSQL (shared instance)
  - Redis (shared instance) 
  - Elasticsearch (optional, but resource-hungry)
  - Sidekiq workers (multiple processes)
  - Web server + streaming API
</code></pre>

<p>The complexity was getting out of hand.</p>

<h2 id="the-migration-process-technical-deep-dive">The Migration Process: Technical Deep-Dive</h2>

<h3 id="testing-phase">Testing Phase</h3>

<p>Before committing fully, I ran GoToSocial parallel to Mastodon for four weeks. This gave me real-world data without burning bridges.</p>

<pre><code class="language-yaml"># compose.yml - GoToSocial setup
services:
  gotosocial:
    image: superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    restart: unless-stopped
    environment:
      GTS_HOST: your-domain.com
      GTS_DB_TYPE: postgres
      GTS_DB_ADDRESS: postgres:5432
      GTS_DB_DATABASE: gotosocial
      GTS_DB_USER: gotosocial
      GTS_DB_PASSWORD: ${GTS_DB_PASSWORD}
      GTS_ACCOUNTS_REGISTRATION_OPEN: false
      GTS_ACCOUNTS_APPROVAL_REQUIRED: true
      GTS_MEDIA_REMOTE_CACHE_DAYS: 7
      GTS_STORAGE_LOCAL_BASE_PATH: /gotosocial/storage
    volumes:
      - ./data:/gotosocial/storage
    networks:
      - traefik_default
      - gotosocial_db
    labels:
      - &#34;traefik.enable=true&#34;
      - &#34;traefik.http.routers.gotosocial.rule=Host(\`your-domain.com\`)&#34;
      - &#34;traefik.http.routers.gotosocial.tls.certresolver=letsencrypt&#34;
    depends_on:
      - postgres

  postgres:
    image: postgres:15-alpine
    container_name: gotosocial_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: gotosocial
      POSTGRES_USER: gotosocial
      POSTGRES_PASSWORD: ${GTS_DB_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    networks:
      - gotosocial_db
</code></pre>

<p><strong>Key Configuration Decisions:</strong></p>
<ul><li>Separate Database: Not sharing with Mastodon to avoid conflicts</li>
<li>7-Day Media Retention: Balancing storage vs. content availability</li>
<li>Traefik Integration: Seamless SSL termination and routing</li>
<li>Bind Mounts: Easy backup/restore without Docker volume complexity</li></ul>

<h3 id="account-migration-the-technical-reality">Account Migration: The Technical Reality</h3>

<p>The actual migration process was surprisingly smooth, but had some gotchas:</p>

<p><strong>1. Mastodon Export</strong></p>

<pre><code class="language-bash"># Export process (takes 24-48 hours for large instances)
# Mastodon Admin → Settings → Import/Export → Request Archive
# Results in: archive-[timestamp].tar.gz
</code></pre>

<p><strong>2. Post Import with slurp</strong></p>

<pre><code class="language-bash"># Install slurp
git clone https://github.com/superseriousbusiness/gotosocial-slurp
cd gotosocial-slurp
go build .

# Import 380 posts from Mastodon archive
./slurp import \
  --gts-endpoint https://your-domain.com \
  --gts-token YOUR_GTS_TOKEN \
  --mastodon-archive archive-20241215.tar.gz \
  --dry-run false

# Results: Perfect import with media, metadata, and timestamps preserved
</code></pre>

<p><strong>3. Account Redirect Setup</strong></p>

<pre><code class="language-bash"># In Mastodon rails console
rails console
account = Account.find_by(username: &#39;your_username&#39;)
account.update(moved_to_account_id: nil) # Clear any previous moves
# Then use Mastodon UI: Settings → Account → Move to different account
</code></pre>

<p><strong>Migration Casualties:</strong></p>
<ul><li>Lost ~50 followers (12% dropout rate)</li>
<li>Likely causes: inactive accounts, app compatibility issues, users who don&#39;t follow redirects</li>
<li>Lesson: Quality over quantity – the remaining followers are actually engaged</li></ul>

<h2 id="performance-benchmarks-numbers-don-t-lie">Performance Benchmarks: Numbers Don&#39;t Lie</h2>

<h3 id="memory-usage-deep-dive">Memory Usage Deep-Dive</h3>

<pre><code class="language-bash"># Monitoring setup with docker stats
docker stats --format &#34;table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}&#34;

# Mastodon (before migration):
CONTAINER       CPU %    MEM USAGE / LIMIT     MEM %
mastodon_web    45.2%    2.1GiB / 8GiB        26.25%
mastodon_sidekiq 23.1%   1.8GiB / 8GiB        22.50%
mastodon_streaming 12.4% 512MiB / 8GiB        6.40%
redis           8.7%     256MiB / 8GiB        3.20%
postgres        15.3%    800MiB / 8GiB        10.00%
elasticsearch   35.8%    1.2GiB / 8GiB        15.00%
Total: ~6.7GB peak usage

# GoToSocial (after migration):
CONTAINER       CPU %    MEM USAGE / LIMIT     MEM %
gotosocial      12.3%    419MiB / 8GiB        5.24%
postgres        3.2%     128MiB / 8GiB        1.60%
fedifetcher     0.8%     3.4MiB / 8GiB        0.04%
gts-holmirdas   1.1%     3.7MiB / 8GiB        0.05%
Total: ~555MB peak usage

RAM Efficiency Gain: 92% reduction (6.7GB → 555MB)
</code></pre>

<p><img src="https://data.klein.ruhr/images/ram-usage-comparison.webp" alt=""></p>

<h3 id="storage-analysis">Storage Analysis</h3>

<pre><code class="language-bash"># Mastodon storage breakdown (after 6 months):
du -sh /opt/mastodon/*
12G     system/  (cache, media_attachments)
45G     public/  (user uploads, cached media)
43G     postgres_data/  (database)
Total: ~100GB with aggressive 7-day cleanup

# GoToSocial storage (extrapolated to same timeframe):
du -sh /opt/gotosocial/*
8.2G    storage/  (all media, database)
26G     postgres_data/  (more efficient schema)
Total: ~35GB with 7-day remote media retention

Storage Efficiency Gain: 65% reduction
</code></pre>

<p><img src="https://data.klein.ruhr/images/storage-comparison.webp" alt=""></p>

<p><strong>Why GoToSocial is so much leaner:</strong></p>
<ul><li>Single binary: No separate web/worker/streaming processes</li>
<li>Efficient database schema: Less metadata overhead per post</li>
<li>Smart media handling: Better deduplication and compression</li>
<li>No Elasticsearch: Full-text search handled by PostgreSQL</li></ul>

<h2 id="federation-performance-quality-over-quantity">Federation Performance: Quality Over Quantity</h2>

<p>One of my biggest concerns was federation reach. Would a single-user GoToSocial instance feel isolated?</p>

<p><strong>The Data After 4 Days:</strong></p>

<pre><code class="language-bash"># Federation stats from GoToSocial admin API
curl -H &#34;Authorization: Bearer $ADMIN_TOKEN&#34; \
  https://your-domain.com/api/v1/admin/accounts/stats

{
  &#34;known_instances&#34;: 2750,
  &#34;federated_posts&#34;: 12847,
  &#34;local_posts&#34;: 380,
  &#34;active_users_week&#34;: 1
}
</code></pre>

<p><strong>Federation happens FAST:</strong></p>
<ul><li>2,750+ instances discovered in 4 days</li>
<li>No traditional relays needed</li>
<li>Content discovery through intelligent RSS integration</li>
<li>Federation quality actually improved (more relevant content, less noise)
<img src="https://data.klein.ruhr/images/federation-growth.webp" alt=""></li></ul>

<h2 id="content-discovery-building-gts-holmirdas">Content Discovery: Building GTS-HolMirDas</h2>

<p>Here&#39;s where things got interesting. Small Fediverse instances suffer from the “empty timeline problem” – without many local users or relay connections, your federated timeline stays pretty barren.</p>

<p><strong>The Technical Challenge</strong></p>

<pre><code class="language-bash"># Traditional approach: Join relays
# Problems:
# - High resource usage
# - Lots of irrelevant content  
# - Reliability issues
# - Limited control over content quality
</code></pre>

<p><strong>My Solution: Intelligent RSS-to-Fediverse Bridge</strong></p>

<p>I built GTS-HolMirDas (German pun meaning “fetch this for me”), an open-source tool that:</p>

<p><strong>Core Architecture:</strong></p>

<pre><code class="language-python"># Simplified architecture overview
class HolMirDas:
    def __init__(self):
        self.rss_feeds = self.load_feed_config()  # 102 RSS feeds
        self.gts_client = GoToSocialClient()
        self.duplicate_tracker = DuplicateDetector()
        self.instance_tracker = InstanceTracker()
    
    def run_cycle(self):
        &#34;&#34;&#34;Main processing loop - runs every hour&#34;&#34;&#34;
        for feed_url, config in self.rss_feeds.items():
            self.process_feed(feed_url, config)
        
        self.cleanup_old_entries()
        self.update_federation_stats()
</code></pre>

<p><strong>Smart Features:</strong></p>
<ul><li>Cross-platform attribution: Properly credits Mastodon, Misskey, Pleroma authors</li>
<li>Duplicate detection: Prevents the same post from appearing multiple times</li>
<li>Instance discovery: Automatically tracks new Fediverse instances</li>
<li>Rate limiting: Respects both RSS and GoToSocial API limits</li></ul>

<p><strong>Performance Metrics</strong></p>

<pre><code class="language-bash"># Real-time stats from my instance:
📊 GTS-HolMirDas Run Statistics:
   ⏱️  Runtime: 4:39 minutes per cycle
   📄 Posts processed: 65 per hour  
   🌐 Current known instances: 2,697
   ➕ New instances discovered: +22 per run
   📡 RSS feeds monitored: 102
   ⚡ Processing rate: 13.9 posts/minute
   🔄 Runs every 60 minutes automatically
</code></pre>

<p><strong>RSS Feed Sources</strong></p>

<pre><code class="language-yaml"># feed_config.yml (excerpt)
feeds:
  # Major Mastodon instances
  - url: &#34;https://mastodon.social/@Gargron.rss&#34;
    platform: &#34;mastodon&#34;
    instance: &#34;mastodon.social&#34;
  
  # Specialized instances  
  - url: &#34;https://fosstodon.org/users/example_user.rss&#34;
    platform: &#34;mastodon&#34;
    instance: &#34;fosstodon.org&#34;
    
  # Other Fediverse platforms
  - url: &#34;https://pixelfed.social/users/photographer.atom&#34;
    platform: &#34;pixelfed&#34;
    instance: &#34;pixelfed.social&#34;
</code></pre>

<p><strong>The Result:</strong> A vibrant federated timeline despite being a single-user instance, with content that&#39;s actually relevant to my interests.</p>

<h2 id="lessons-learned-the-good-the-bad-the-ugly">Lessons Learned: The Good, The Bad, The Ugly</h2>

<h3 id="what-works-brilliantly">What Works Brilliantly ✅</h3>

<p><strong>Resource Efficiency is Game-Changing</strong></p>
<ul><li>Single binary vs. multiple processes: Much simpler debugging</li>
<li>Memory usage stays consistently under 500MB even under load</li>
<li>CPU usage rarely spikes above 15% during federation heavy-lifting</li>
<li>Storage growth is predictable and manageable</li></ul>

<p><strong>Federation Just Works™</strong></p>
<ul><li>No manual instance discovery needed</li>
<li>ActivityPub compatibility is excellent</li>
<li>Cross-platform federation (Mastodon ↔ Misskey ↔ Pleroma) seamless</li>
<li>Better federation performance than expected</li></ul>

<p><strong>Operational Simplicity</strong></p>

<pre><code class="language-bash"># Mastodon maintenance (weekly):
docker compose exec web bundle exec rake mastodon:media:remove_remote
docker compose exec web bundle exec rake mastodon:preview_cards:remove_old
docker compose exec web bundle exec rake mastodon:feeds:build
# + manual Elasticsearch index management
# + Sidekiq queue monitoring
# + Redis memory management

# GoToSocial maintenance (monthly):
# That&#39;s it. Seriously.
</code></pre>

<h3 id="the-challenges">The Challenges ⚠️</h3>

<p><strong>Client Ecosystem</strong></p>
<ul><li>Mobile Apps: Hit-or-miss compatibility
<ul><li>iOS Tusker: Works perfectly, actively maintained</li>
<li>iOS Ice Cubes: Notification issues, otherwise great</li>
<li>Android Tusky: Mostly works, some UI quirks</li>
<li>Android Fedilab: Best overall experience</li></ul></li>
<li>Web Clients: Need self-hosting for optimal experience</li></ul>

<p><strong>Missing Mastodon Features (That You Might Miss)</strong></p>
<ul><li>Advanced search capabilities (no Elasticsearch)</li>
<li>Trending hashtags/posts</li>
<li>Some admin tools are more basic</li>
<li>Limited API endpoints compared to Mastodon</li></ul>

<p><strong>Federation Edge Cases</strong></p>

<pre><code class="language-bash"># Occasional federation hiccups I encountered:
- Large instances sometimes lag on post delivery (not GTS fault)
- Some Mastodon instances reject GTS posts (rare, usually config issues)
- Media federation can be slow during high-traffic periods
</code></pre>

<h2 id="mobile-apps-real-world-testing">Mobile Apps: Real-World Testing</h2>

<p><strong>iOS Testing Results:</strong></p>

<p><strong>Tusker: 9/10</strong></p>
<ul><li>✅ Full feature support</li>
<li>✅ Reliable notifications</li>
<li>✅ Good performance</li>
<li>❌ Paid app ($3.99)</li></ul>

<p><strong>Ice Cubes: 7/10</strong></p>
<ul><li>✅ Beautiful interface</li>
<li>✅ Active development</li>
<li>❌ Notification reliability issues</li>
<li>❌ Some API compatibility gaps</li></ul>

<p><strong>Self-Hosted Web Clients:</strong></p>

<pre><code class="language-yaml"># My current setup
services:
  phanpy:
    image: cheeaun/phanpy:latest
    container_name: phanpy
    environment:
      DEFAULT_INSTANCE: your-domain.com
    labels:
      - &#34;traefik.http.routers.phanpy.rule=Host(\`social.your-domain.com\`)&#34;
  
  elk:
    image: elkzone/elk:latest
    container_name: elk
    environment:
      DEFAULT_SERVER: your-domain.com
    labels:
      - &#34;traefik.http.routers.elk.rule=Host(\`elk.your-domain.com\`)&#34;
</code></pre>

<p><strong>Client Comparison:</strong></p>
<ul><li>Phanpy: Better features, more Mastodon-like, slightly dated UI</li>
<li>Elk: Gorgeous interface, modern UX, fewer power-user features</li>
<li>Official GTS Web UI: Basic but functional, good for admin tasks</li></ul>

<h2 id="cost-analysis">Cost Analysis</h2>

<p><strong>Comparison with Mastodon hosting costs:</strong></p>

<p><strong>Mastodon requirements:</strong></p>
<ul><li>Minimum: 4GB RAM, 2 cores</li>
<li>Monthly VPS cost: $20-40</li>
<li>Additional services needed: Redis, Elasticsearch (optional but recommended)</li></ul>

<p><strong>GoToSocial requirements:</strong></p>
<ul><li>Minimum: 1GB RAM, 1 core</li>
<li>Monthly VPS cost: $5-10</li>
<li>Additional services: Just PostgreSQL (can use SQLite for single-user)</li></ul>

<p><strong>Annual savings: $180-360 per year</strong></p>

<p><img src="https://data.klein.ruhr/images/cost-comparison.webp" alt=""></p>

<h2 id="backup-strategy-lessons-from-migration">Backup Strategy: Lessons from Migration</h2>

<pre><code class="language-bash">#!/bin/bash
# My automated backup script
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=&#34;/opt/backups/gotosocial&#34;

# Database backup
docker compose exec -T postgres pg_dump -U gotosocial gotosocial &gt; \
  &#34;$BACKUP_DIR/db_backup_$DATE.sql&#34;

# Media and storage backup
tar -czf &#34;$BACKUP_DIR/storage_backup_$DATE.tar.gz&#34; \
  /opt/gotosocial/data/

# Configuration backup  
cp compose.yml &#34;$BACKUP_DIR/compose_backup_$DATE.yml&#34;

# Keep only last 7 days of backups
find &#34;$BACKUP_DIR&#34; -name &#34;*.sql&#34; -mtime +7 -delete
find &#34;$BACKUP_DIR&#34; -name &#34;*.tar.gz&#34; -mtime +7 -delete

echo &#34;Backup completed: $DATE&#34;
</code></pre>

<p>Recovery tested and verified – I&#39;ve successfully restored from backup during my testing phase. The process is straightforward and well-documented.</p>

<h2 id="the-tools-that-make-it-work">The Tools That Make It Work</h2>

<p>All the supporting tools I built are open source and production-ready:</p>

<h3 id="gts-holmirdas">GTS-HolMirDas</h3>

<p><strong>Repository:</strong><a href="https://git.klein.ruhr/matthias/gts-holmirdas?ref=blog.klein.ruhr">https://git.klein.ruhr/matthias/gts-holmirdas</a></p>

<p><strong>Features:</strong></p>
<ul><li>102 RSS feed sources from diverse Fediverse instances</li>
<li>Cross-platform attribution (Mastodon, Misskey, Pleroma, Pixelfed)</li>
<li>Intelligent duplicate detection</li>
<li>Automatic instance discovery</li>
<li>Rate limiting and error handling</li>
<li>Docker-ready deployment</li></ul>

<p><strong>Setup in 5 minutes:</strong></p>

<pre><code class="language-bash">git clone https://git.your-domain.com/username/gts-holmirdas.git
cd gts-holmirdas
cp config.example.yml config.yml
# Edit config.yml with your GTS instance details
docker compose up -d
</code></pre>

<h3 id="fedifetcher-integration">FediFetcher Integration</h3>

<p><strong>Repository:</strong> blog.thms.uk/fedifetcher</p>

<p>While not my creation, FediFetcher perfectly complements GoToSocial for thread completion:</p>

<pre><code class="language-yaml"># compose.yml addition
services:
  fedifetcher:
    image: nanos/fedifetcher:latest
    container_name: fedifetcher
    environment:
      - HOME_INSTANCE=your-domain.com
      - ACCESS_TOKEN=${FEDIFETCHER_TOKEN}
      - REPLY_INTERVAL_HOURS=1
      - BACKFILL_HOURS=24
    restart: unless-stopped
</code></pre>

<p><strong>What it does:</strong></p>
<ul><li>Fetches missing replies to posts in your timeline</li>
<li>Completes conversation threads automatically</li>
<li>Backfills content from instances your server hasn&#39;t seen yet</li>
<li>Minimal resource usage (3-4MB RAM)</li></ul>

<h2 id="monitoring-and-observability">Monitoring and Observability</h2>

<pre><code class="language-yaml"># Prometheus metrics (optional but recommended)
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - &#39;--config.file=/etc/prometheus/prometheus.yml&#39;
      - &#39;--storage.tsdb.path=/prometheus&#39;
      - &#39;--web.console.libraries=/etc/prometheus/console_libraries&#39;

# prometheus.yml
scrape_configs:
  - job_name: &#39;gotosocial&#39;
    static_configs:
      - targets: [&#39;gotosocial:8080&#39;]
    metrics_path: &#39;/metrics&#39;
</code></pre>

<p><strong>Key metrics to monitor:</strong></p>
<ul><li>Memory usage trends</li>
<li>Database growth rate</li>
<li>Federation success rates</li>
<li>API response times</li>
<li>Storage utilization</li></ul>

<h2 id="migration-gotchas-and-pro-tips">Migration Gotchas and Pro Tips</h2>

<h3 id="things-i-wish-i-d-known-before-starting">Things I Wish I&#39;d Known Before Starting</h3>

<p><strong>1. Test Federation Early</strong></p>

<pre><code class="language-bash"># Verify federation is working before migrating followers
curl -H &#34;Accept: application/activity+json&#34; \
  https://your-domain.com/users/your_username

# Should return ActivityPub JSON, not HTML
</code></pre>

<p><strong>2. Mobile App Testing is Critical</strong><br>
Don&#39;t assume your favorite Mastodon app will work perfectly. Test extensively with your actual usage patterns before committing.</p>

<p><strong>3. Content Discovery Takes Time</strong><br>
The federated timeline won&#39;t be instantly populated. Plan for a “quiet period” of 1-2 weeks while content discovery tools build up your federation network.</p>

<p><strong>4. Backup Before Migration</strong></p>

<pre><code class="language-bash"># Create a complete Mastodon backup before starting
docker compose exec web rake mastodon:backup:create
# This creates a full export in /app/backup/
</code></pre>

<p><strong>5. Domain Considerations</strong><br>
If you&#39;re changing domains during migration (like I did), be prepared for some federation hiccups. Some instances cache domain information aggressively.</p>

<h2 id="performance-tuning-tips">Performance Tuning Tips</h2>

<pre><code class="language-bash"># PostgreSQL optimization for GoToSocial
# Add to postgres container environment:
POSTGRES_INITDB_ARGS: &#34;--data-checksums&#34;
# In postgresql.conf:
shared_buffers = 128MB          # For 1GB RAM systems
effective_cache_size = 512MB    # 3/4 of available RAM
work_mem = 4MB                  # Per query operation
maintenance_work_mem = 64MB     # For maintenance operations
</code></pre>

<pre><code class="language-bash"># GoToSocial-specific optimizations:
# In your .env file:
GTS_CACHE_MEMORY_TARGET=100MB           # Adjust based on available RAM
GTS_MEDIA_REMOTE_CACHE_DAYS=7           # Balance storage vs. availability
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=false     # Disable if not needed
GTS_WEB_ASSET_BASE_DIR=/gotosocial/web  # Serve assets efficiently
</code></pre>

<h2 id="community-impact-and-future-plans">Community Impact and Future Plans</h2>

<h3 id="what-this-migration-means-for-the-fediverse">What This Migration Means for the Fediverse</h3>

<p><strong>Resource Democracy:</strong> By proving that high-quality Fediverse instances can run on minimal resources, we&#39;re making decentralization more accessible to everyone.</p>

<p><strong>Tool Ecosystem:</strong> The tools developed for this migration (especially GTS-HolMirDas) are helping other small instance operators solve the same content discovery challenges.</p>

<p><strong>Documentation:</strong> This migration provides a real-world data point for others considering similar moves.</p>

<h3 id="future-development-plans">Future Development Plans</h3>

<p><strong>GTS-HolMirDas Roadmap:</strong></p>
<ul><li>Web-based configuration interface</li>
<li>Better RSS feed quality scoring</li>
<li>Integration with more Fediverse platforms (PeerTube, Funkwhale)</li>
<li>Machine learning-based content relevance filtering</li>
<li>Multi-instance support for hosting providers</li></ul>

<p><strong>Community Feedback Integration:</strong> Based on initial user feedback, the most requested features are:</p>
<ul><li>Easier RSS feed discovery and management</li>
<li>Better content filtering options</li>
<li>Integration with existing relay networks</li>
<li>Automated instance health monitoring</li></ul>

<h2 id="bottom-line-the-numbers-don-t-lie">Bottom Line: The Numbers Don&#39;t Lie</h2>

<p><strong>Migration Summary:</strong></p>
<ul><li>Timeline: 4 weeks of testing, 2 days for full migration</li>
<li>Resource savings: 92% RAM reduction, 65% storage reduction</li>
<li>Follower retention: 88% (better than expected)</li>
<li>Federation growth: 2,750+ instances in first week</li>
<li>Operational complexity: Significantly reduced</li>
<li>Cost savings: $180-360 annually in hosting costs</li>
<li>Satisfaction level: 9.5/10 (would migrate again)</li></ul>

<p><strong>The Real Kicker:</strong> GoToSocial doesn&#39;t just match Mastodon&#39;s functionality at lower resource usage – in many ways, it performs better. The federated timeline is more relevant, the interface is faster, and the operational overhead is practically non-existent.</p>

<p><strong>You should definitely consider GoToSocial if:</strong></p>
<ul><li>You&#39;re running a single-user or small instance</li>
<li>Resource efficiency matters to you</li>
<li>You value operational simplicity</li>
<li>You&#39;re comfortable with slightly less mature tooling</li>
<li>You want to contribute to Fediverse diversity</li></ul>

<p><strong>Stick with Mastodon if:</strong></p>
<ul><li>You&#39;re running a large community (100+ users)</li>
<li>You need enterprise-grade admin tools</li>
<li>Your users depend on specific Mastodon features</li>
<li>You have abundant server resources</li>
<li>Stability trumps efficiency for your use case</li></ul>

<p><strong>My recommendation:</strong> Set up a test GoToSocial instance and run it parallel to your existing setup for a month. The resource cost is minimal, and you&#39;ll get real-world data for your specific use case.</p>

<p>The Fediverse is strongest when it&#39;s diverse – different platforms solving different problems for different communities. GoToSocial has earned its place in that ecosystem.</p>

<hr>

<h2 id="resources-and-links">Resources and Links</h2>

<h3 id="essential-tools">Essential Tools</h3>
<ul><li><strong>GoToSocial:</strong><a href="https://gotosocial.org/?ref=blog.klein.ruhr">https://gotosocial.org</a></li>
<li><strong>GTS-HolMirDas:</strong><a href="https://git.klein.ruhr/matthias/gts-holmirdas?ref=blog.klein.ruhr">https://git.klein.ruhr/matthias/gts-holmirdas</a></li>
<li><strong>FediFetcher:</strong><a href="https://blog.thms.uk/fedifetcher?ref=blog.klein.ruhr">https://blog.thms.uk/fedifetcher</a></li>
<li><strong>Slurp (Post Migration):</strong><a href="https://github.com/VyrCossont/slurp?ref=blog.klein.ruhr">https://github.com/VyrCossont/slurp</a></li></ul>

<h3 id="mobile-apps-that-work-well">Mobile Apps That Work Well</h3>
<ul><li><strong>iOS Tusker:</strong> App Store (paid, highly recommended)</li>
<li><strong>iOS Ice Cubes:</strong> App Store (free, some quirks)</li>
<li><strong>Android Fedilab:</strong> F-Droid/Play Store (best Android option)</li>
<li><strong>Android Tusky:</strong> F-Droid/Play Store (decent alternative)</li></ul>

<h3 id="web-clients-self-hosted">Web Clients (Self-Hosted)</h3>
<ul><li><strong>Phanpy:</strong><a href="https://github.com/cheeaun/phanpy?ref=blog.klein.ruhr">https://github.com/cheeaun/phanpy</a></li>
<li><strong>Elk:</strong><a href="https://github.com/elk-zone/elk?ref=blog.klein.ruhr">https://github.com/elk-zone/elk</a></li></ul>

<h3 id="community">Community</h3>
<ul><li><strong>GoToSocial Documentation:</strong><a href="https://docs.gotosocial.org/?ref=blog.klein.ruhr">https://docs.gotosocial.org</a></li>
<li><strong>Matrix Support:</strong> <a href="https://blog.klein.ruhr/tag:gotosocial" class="hashtag"><span>#</span><span class="p-category">gotosocial</span></a>:superseriousbusiness.org</li>
<li><strong>GitHub Issues:</strong><a href="https://github.com/superseriousbusiness/gotosocial?ref=blog.klein.ruhr">https://github.com/superseriousbusiness/gotosocial</a></li></ul>

<hr>

<p><strong>Questions? Feedback?</strong> Find me on the Fediverse at <a href="https://me.klein.ruhr/@matthias?ref=blog.klein.ruhr"><a href="https://blog.klein.ruhr/@/matthias@me.klein.ruhr" class="u-url mention">@<span>matthias@me.klein.ruhr</span></a></a> – I&#39;m always happy to discuss self-hosting, the Fediverse, and efficient technology solutions!</p>

<p>If this post helped you with your own migration, consider starring the GTS-HolMirDas repository or contributing to the GoToSocial project. The Fediverse grows stronger with every successful self-hosted instance.</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/gotosocial-ready-for-prime-time</guid>
      <pubDate>Mon, 04 Aug 2025 02:17:17 +0000</pubDate>
    </item>
    <item>
      <title>Why I Use OpenPGP and How You Can Too</title>
      <link>https://blog.klein.ruhr/why-i-use-openpgp-and-how-you-can-too</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/openpgp.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;We live in a time in which our most personal conversations happen through screens and keyboards. Every email, message, and document we send travels through countless servers before reaching its destination. As someone who works in IT, I&#39;ve seen firsthand how easily digital communication can be intercepted or compromised.&#xA;&#xA;That&#39;s why I rely on OpenPGP – not because I have anything to hide, but because I value the fundamental right to private communication. It&#39;s one of the most robust encryption standards available, protecting digital conversations for over three decades.&#xA;&#xA;OpenPGP: More Than Just Encryption&#xA;&#xA;OpenPGP operates on a beautifully simple concept. Imagine you have a special lockbox that only you can open. You can give copies of the lock to anyone, but the key stays with you. When someone wants to send you a secret message, they lock it with your lock and send it. Only your key can open it.&#xA;&#xA;This is essentially how OpenPGP works, but with mathematical precision. You generate two mathematically linked keys: a public key you share freely, and a private key that stays securely with you. The mathematics is so solid that even powerful computers would need centuries to break properly implemented encryption.&#xA;&#xA;Beyond hiding messages, OpenPGP also lets you sign them, creating a digital fingerprint that proves authenticity and prevents tampering. Think of it as an impossible-to-forge tamper-evident seal.&#xA;!--more--&#xA;---&#xA;&#xA;🎯 Try It Yourself&#xA;&#xA;Want to see how OpenPGP encryption works in practice? &#xA;&#xA;→ Interactive OpenPGP Demo&#xA;&#xA;Experience the complete encryption workflow: write a message, encrypt it with a public key, and decrypt it with the private key.&#xA;&#xA;---&#xA;&#xA;Why I Choose OpenPGP Over Other Solutions&#xA;&#xA;The digital landscape is full of encrypted messaging apps, so why OpenPGP? The answer is control and longevity. Most encrypted services are controlled by companies that could change policies, be acquired, or shut down. With OpenPGP, I own my keys and control my encryption. No company can revoke my ability to communicate securely.&#xA;&#xA;OpenPGP has proven its resilience over decades of scrutiny by security experts. This isn&#39;t trendy new encryption with potential undiscovered flaws – it&#39;s battle-tested technology protecting everything from personal emails to classified communications.&#xA;&#xA;When OpenPGP Makes a Real Difference&#xA;&#xA;Recently, I collaborated on a sensitive project involving technical specifications. Using regular email would have meant this information could be accessed by anyone with administrative access to email servers. With OpenPGP, only my intended recipients could read the details.&#xA;&#xA;I&#39;ve also helped friends protect personal information during legal matters or medical communications. When sending documents containing social security numbers or financial details, that extra protection becomes crucial. For journalists and activists protecting sources, OpenPGP isn&#39;t just about privacy – it&#39;s about safety.&#xA;&#xA;Getting Started: The Practical Side&#xA;&#xA;Modern tools have made OpenPGP much more approachable. Windows users can use Gpg4win with its user-friendly Kleopatra interface. Mac users have GPG Suite, which integrates with the built-in Mail app. Linux users often find GnuPG already installed.&#xA;&#xA;The key generation process is straightforward: provide your name and email, choose settings (defaults work fine), and create a strong passphrase. This passphrase protects your private key if someone gains computer access.&#xA;&#xA;Mobile support has improved significantly. Android users have OpenKeychain, which integrates with various apps. iOS is more challenging due to platform restrictions, but ProtonMail provides encrypted email with built-in OpenPGP support.&#xA;&#xA;Essential Key Management&#xA;&#xA;Think of your private key as the master key to your digital identity. Create a revocation certificate immediately after generating keys – this tells the world to stop trusting your key if it&#39;s compromised. Store this certificate securely offline.&#xA;&#xA;Back up your keys in multiple secure locations, including offline storage. Set keys to expire after reasonable periods, forcing active renewal as good security practice. Share your public key via key servers but always verify fingerprints through separate channels like phone calls to prevent man-in-the-middle attacks.&#xA;&#xA;Building Good Security Habits &amp; Taking Control&#xA;&#xA;OpenPGP is part of a larger security puzzle. Use strong, unique passphrases and consider hardware security keys for additional protection. Keep software updated and be careful entering passphrases on public computers or unsecured networks.&#xA;&#xA;Device security matters too: full-disk encryption, antivirus software, and firewalls protect where your keys are stored. Privacy isn&#39;t about hiding something – it&#39;s about controlling your personal information in a world of data breaches and expanding surveillance.&#xA;&#xA;Taking the First Step&#xA;&#xA;Start small: generate a key pair, share your public key with a trusted colleague, and try exchanging encrypted messages. The learning curve isn&#39;t steep, and the peace of mind is worth the effort.&#xA;&#xA;Privacy is a practice, not a product. OpenPGP provides the tools, but developing good security habits is ongoing. Your communications are worth protecting – OpenPGP is ready to protect yours too.&#xA;&#xA;Useful Resources&#xA;OpenPGP Official Site: openpgp.org&#xA;Email Security Guide: EFF&#39;s Surveillance Self-Defense&#xA;Key Management Best Practices: Riseup&#39;s OpenPGP Guide&#xA;Academic Research: The PGP Paradigm]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/openpgp.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
We live in a time in which our most personal conversations happen through screens and keyboards. Every email, message, and document we send travels through countless servers before reaching its destination. As someone who works in IT, I&#39;ve seen firsthand how easily digital communication can be intercepted or compromised.</p>

<p>That&#39;s why I rely on <a href="https://www.openpgp.org/">OpenPGP</a> – not because I have anything to hide, but because I value the fundamental right to private communication. It&#39;s one of the most robust encryption standards available, protecting digital conversations for over three decades.</p>

<h2 id="openpgp-more-than-just-encryption">OpenPGP: More Than Just Encryption</h2>

<p>OpenPGP operates on a beautifully simple concept. Imagine you have a special lockbox that only you can open. You can give copies of the lock to anyone, but the key stays with you. When someone wants to send you a secret message, they lock it with your lock and send it. Only your key can open it.</p>

<p>This is essentially how OpenPGP works, but with mathematical precision. You generate two mathematically linked keys: a public key you share freely, and a private key that stays securely with you. The mathematics is so solid that even powerful computers would need centuries to break properly implemented encryption.</p>

<p>Beyond hiding messages, OpenPGP also lets you sign them, creating a digital fingerprint that proves authenticity and prevents tampering. Think of it as an impossible-to-forge tamper-evident seal.
</p>

<hr>

<h2 id="try-it-yourself">🎯 Try It Yourself</h2>

<p>Want to see how OpenPGP encryption works in practice?</p>

<p><strong><a href="https://data.klein.ruhr/tools/openpgp-demo.html">→ Interactive OpenPGP Demo</a></strong></p>

<p>Experience the complete encryption workflow: write a message, encrypt it with a public key, and decrypt it with the private key.</p>

<hr>

<h2 id="why-i-choose-openpgp-over-other-solutions">Why I Choose OpenPGP Over Other Solutions</h2>

<p>The digital landscape is full of encrypted messaging apps, so why OpenPGP? The answer is control and longevity. Most encrypted services are controlled by companies that could change policies, be acquired, or shut down. With OpenPGP, I own my keys and control my encryption. No company can revoke my ability to communicate securely.</p>

<p>OpenPGP has proven its resilience over decades of scrutiny by security experts. This isn&#39;t trendy new encryption with potential undiscovered flaws – it&#39;s battle-tested technology protecting everything from personal emails to classified communications.</p>

<p><img src="https://data.klein.ruhr/images/pgp1.webp" alt=""></p>

<h2 id="when-openpgp-makes-a-real-difference">When OpenPGP Makes a Real Difference</h2>

<p>Recently, I collaborated on a sensitive project involving technical specifications. Using regular email would have meant this information could be accessed by anyone with administrative access to email servers. With OpenPGP, only my intended recipients could read the details.</p>

<p>I&#39;ve also helped friends protect personal information during legal matters or medical communications. When sending documents containing social security numbers or financial details, that extra protection becomes crucial. For journalists and activists protecting sources, OpenPGP isn&#39;t just about privacy – it&#39;s about safety.</p>

<p><img src="https://data.klein.ruhr/images/pgp2.webp" alt=""></p>

<h2 id="getting-started-the-practical-side">Getting Started: The Practical Side</h2>

<p>Modern tools have made OpenPGP much more approachable. Windows users can use <a href="https://www.gpg4win.org/">Gpg4win</a> with its user-friendly Kleopatra interface. Mac users have <a href="https://gpgtools.org/">GPG Suite</a>, which integrates with the built-in Mail app. Linux users often find <a href="https://gnupg.org/">GnuPG</a> already installed.</p>

<p>The key generation process is straightforward: provide your name and email, choose settings (defaults work fine), and create a strong passphrase. This passphrase protects your private key if someone gains computer access.</p>

<p>Mobile support has improved significantly. Android users have <a href="https://www.openkeychain.org/">OpenKeychain</a>, which integrates with various apps. iOS is more challenging due to platform restrictions, but <a href="https://protonmail.com/">ProtonMail</a> provides encrypted email with built-in OpenPGP support.</p>

<p><img src="https://data.klein.ruhr/images/pgp3.webp" alt=""></p>

<h2 id="essential-key-management">Essential Key Management</h2>

<p>Think of your private key as the master key to your digital identity. Create a revocation certificate immediately after generating keys – this tells the world to stop trusting your key if it&#39;s compromised. Store this certificate securely offline.</p>

<p>Back up your keys in multiple secure locations, including offline storage. Set keys to expire after reasonable periods, forcing active renewal as good security practice. Share your public key via <a href="https://keys.openpgp.org/">key servers</a> but always verify fingerprints through separate channels like phone calls to prevent man-in-the-middle attacks.</p>

<h2 id="building-good-security-habits-taking-control">Building Good Security Habits &amp; Taking Control</h2>

<p>OpenPGP is part of a larger security puzzle. Use strong, unique passphrases and consider hardware security keys for additional protection. Keep software updated and be careful entering passphrases on public computers or unsecured networks.</p>

<p>Device security matters too: full-disk encryption, antivirus software, and firewalls protect where your keys are stored. Privacy isn&#39;t about hiding something – it&#39;s about controlling your personal information in a world of data breaches and expanding surveillance.</p>

<p><img src="https://data.klein.ruhr/images/pgp4.webp" alt=""></p>

<h2 id="taking-the-first-step">Taking the First Step</h2>

<p>Start small: generate a key pair, share your public key with a trusted colleague, and try exchanging encrypted messages. The learning curve isn&#39;t steep, and the peace of mind is worth the effort.</p>

<p>Privacy is a practice, not a product. OpenPGP provides the tools, but developing good security habits is ongoing. Your communications are worth protecting – OpenPGP is ready to protect yours too.</p>

<h2 id="useful-resources">Useful Resources</h2>
<ul><li><strong>OpenPGP Official Site</strong>: <a href="https://www.openpgp.org/">openpgp.org</a></li>
<li><strong>Email Security Guide</strong>: <a href="https://ssd.eff.org/en/module/how-use-pgp-windows">EFF&#39;s Surveillance Self-Defense</a></li>
<li><strong>Key Management Best Practices</strong>: <a href="https://riseup.net/en/security/message-security/openpgp/best-practices">Riseup&#39;s OpenPGP Guide</a></li>
<li><strong>Academic Research</strong>: <a href="https://www.schneier.com/academic/pgp/">The PGP Paradigm</a></li></ul>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/why-i-use-openpgp-and-how-you-can-too</guid>
      <pubDate>Thu, 24 Jul 2025 02:17:17 +0000</pubDate>
    </item>
    <item>
      <title>✉️ Sending mails from scripts failed? – How to fix it with mutt</title>
      <link>https://blog.klein.ruhr/sending-mails-from-scripts-failed</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/mailswithmutt.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;If you&#39;ve ever tried to send emails from a shell script, you&#39;ve likely run into tools like mail, mailx, or even sendmail. They seem easy at first glance – but quickly become frustrating: strange formatting, attachment issues, encoding errors, and cryptic failure messages.&#xA;&#xA;💨 Common Issues with mailx&#xA;Different implementations across distros (BSD vs. Heirloom vs. GNU)&#xA;Inconsistent or broken attachment support via -a&#xA;Poor handling of UTF-8 content&#xA;No built-in HTML mail support&#xA;Lack of clear error messages or logs&#xA;!--more--&#xA;✅ A Better Way: Using mutt&#xA;&#xA;mutt is primarily a full-featured email client, but it also works reliably in scripts. Here&#39;s why it&#39;s a great choice:&#xA;&#xA;Reliable attachment support using -a&#xA;Handles UTF-8 and HTML content without hacks&#xA;Consistent behavior across environments&#xA;Easy to debug with verbose logging if needed&#xA;!--more--&#xA;🛠️ Example: Send HTML Mail with Markdown Attachment&#xA;&#xA;mutt -e &#34;set from=watchdog@example.com&#34; \&#xA;     -s &#34;📦 LXC Docker Update Report&#34; \&#xA;     -a &#34;/path/to/attachment.md&#34; -- admin@example.com \&#xA;     &lt; /path/to/email-body.html&#xA;&#xA;⚙️ Setting up msmtp as SMTP Relay for mutt&#xA;&#xA;To send mail, mutt needs a mail transport agent. msmtp is a lightweight and easy-to-use solution:&#xA;&#xA;~/.msmtprc&#xA;defaults&#xA;auth           on&#xA;tls            on&#xA;tlstrustfile /etc/ssl/certs/ca-certificates.crt&#xA;logfile        ~/.msmtp.log&#xA;&#xA;account        default&#xA;host           mail.example.com&#xA;port           465&#xA;from           watchdog@example.com&#xA;user           watchdog@example.com&#xA;passwordeval   &#34;gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.msmtp-password.gpg&#34;&#xA;Make sure the config file is secure:&#xA;&#xA;chmod 600 ~/.msmtprc&#xA;And configure mutt to use msmtp:&#xA;&#xA;~/.muttrc&#xA;set sendmail=&#34;/usr/bin/msmtp&#34;&#xA;set use_from=yes&#xA;set realname=&#34;LXC Docker Watchdog&#34;&#xA;set from=&#34;watchdog@example.com&#34;&#xA;&#xA;📋 Summary&#xA;&#xA;If you care about sending reliable emails from scripts – especially with attachments or HTML formatting – then mutt is your friend. Once set up with msmtp, it just works.&#xA;&#xA;No more weird encodings, no more broken attachments, no more surprises.&#xA;&#xA;Just clean, structured, script-friendly email.]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/mailswithmutt.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
If you&#39;ve ever tried to send emails from a shell script, you&#39;ve likely run into tools like <code>mail</code>, <code>mailx</code>, or even <code>sendmail</code>. They seem easy at first glance – but quickly become frustrating: strange formatting, attachment issues, encoding errors, and cryptic failure messages.</p>

<h3 id="common-issues-with-mailx">💨 Common Issues with <code>mailx</code></h3>
<ul><li>Different implementations across distros (BSD vs. Heirloom vs. GNU)</li>
<li>Inconsistent or broken attachment support via <code>-a</code></li>
<li>Poor handling of UTF-8 content</li>
<li>No built-in HTML mail support</li>

<li><p>Lack of clear error messages or logs
</p>

<h3 id="a-better-way-using-mutt">✅ A Better Way: Using <code>mutt</code></h3></li></ul>

<p><code>mutt</code> is primarily a full-featured email client, but it also works reliably in scripts. Here&#39;s why it&#39;s a great choice:</p>
<ul><li><strong>Reliable attachment support</strong> using <code>-a</code></li>
<li><strong>Handles UTF-8 and HTML</strong> content without hacks</li>
<li><strong>Consistent behavior</strong> across environments</li>

<li><p><strong>Easy to debug</strong> with verbose logging if needed
</p>

<h3 id="example-send-html-mail-with-markdown-attachment">🛠️ Example: Send HTML Mail with Markdown Attachment</h3></li></ul>

<pre><code class="language-bash">mutt -e &#34;set from=watchdog@example.com&#34; \
     -s &#34;📦 LXC Docker Update Report&#34; \
     -a &#34;/path/to/attachment.md&#34; -- admin@example.com \
     &lt; /path/to/email-body.html
</code></pre>

<h3 id="setting-up-msmtp-as-smtp-relay-for-mutt">⚙️ Setting up <code>msmtp</code> as SMTP Relay for <code>mutt</code></h3>

<p>To send mail, <code>mutt</code> needs a mail transport agent. <code>msmtp</code> is a lightweight and easy-to-use solution:</p>

<pre><code class="language-bash"># ~/.msmtprc
defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        ~/.msmtp.log

account        default
host           mail.example.com
port           465
from           watchdog@example.com
user           watchdog@example.com
passwordeval   &#34;gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.msmtp-password.gpg&#34;
</code></pre>

<p>Make sure the config file is secure:</p>

<pre><code>chmod 600 ~/.msmtprc
</code></pre>

<p>And configure <code>mutt</code> to use <code>msmtp</code>:</p>

<pre><code class="language-bash"># ~/.muttrc
set sendmail=&#34;/usr/bin/msmtp&#34;
set use_from=yes
set realname=&#34;LXC Docker Watchdog&#34;
set from=&#34;watchdog@example.com&#34;
</code></pre>

<h3 id="summary">📋 Summary</h3>

<p>If you care about sending reliable emails from scripts – especially with attachments or HTML formatting – then <code>mutt</code> is your friend. Once set up with <code>msmtp</code>, it just works.</p>

<p>No more weird encodings, no more broken attachments, no more surprises.</p>

<p>Just clean, structured, script-friendly email.</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/sending-mails-from-scripts-failed</guid>
      <pubDate>Wed, 23 Apr 2025 02:17:17 +0000</pubDate>
    </item>
    <item>
      <title>List of my default apps in 2025</title>
      <link>https://blog.klein.ruhr/list-of-my-default-apps-in-2025</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/appsiselfhost.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;When I started blogging (sometime in the 2000&#39;s), a popular tradition alongside the blogroll - a curated list of recommended blogs - was the “blog baton.” Bloggers would write about a specific topic and then pass it on to others, encouraging them to share their own perspectives.&#xA;&#xA;These year-end and New Year lists of standard apps used by bloggers feel like a modern take on that tradition, as they pop up across the web. While I wasn’t directly invited to join the conversation this time, I decided to jump in after seeing Oliver’s take on the topic, which caught my interest.&#xA;!--more--&#xA;📨 Mail Service: self-hosted Mailcow&#xA;📮 Mail Client: Thunderbird (macOS), Apple Mail (iOS), SOGo (Web)&#xA;📝 Notizen: Obsidian&#xA;✅ To-Do: Obsidian&#xA;🌅 Fotoarchiv: iCloud Photos / Immich (self-hosted)&#xA;📆 Kalender: Apple Calendar&#xA;📁 Cloud-Speicher: Nextcloud&#xA;📖 RSS-Dienst: FreshRSS (self-hosted)&#xA;🙍🏻‍♂️ Kontakte: Apple Contacts&#xA;🌐 Browser: Safari / Brave&#xA;💬 Chat: preferred Signal &amp; Messages (also Threema &amp; WhatsApp 😔)&#xA;🛒 Einkaufslisten: Apple Reminders &amp; Obsidian&#xA;🔎 Suche: Whoogle (self-hosted)&#xA;📚 Lesen: Kindle Paperwhite &amp; Apple Books&#xA;📰 Nachrichten: Reeder Classic&#xA;🎵 Musik: Apple Music&#xA;🎤 Podcasts: Apple Podcasts&#xA;🌤️ Wetter: Apple Weather&#xA;🔐 Passwortverwaltung: Bitwarden (self-hosted)&#xA;🧮 Code-Editor: CotEditor&#xA;&#xA;Inspired by Oliver, I’ve decided to make this list an annual tradition and plan to publish it regularly at the turn of the year. To ensure I don’t forget, I’ve already set a calendar reminder - because let’s be honest, without it, there’s almost no chance this would happen again. 🤪&#xA;&#xA;The idea for this type of article originally came from Robb Knights, who continues to lead the “App Defaults” project. He curates and collects RSS feeds from people across the web, sharing which default apps and services they rely on.&#xA;&#xA;If you enjoyed this article, why not join in? The concept practically begs for participation. 😉]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/appsiselfhost.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
When I started blogging (sometime in the 2000&#39;s), a popular tradition alongside the blogroll – a curated list of recommended blogs – was the “blog baton.” Bloggers would write about a specific topic and then pass it on to others, encouraging them to share their own perspectives.</p>

<p>These year-end and New Year lists of standard apps used by bloggers feel like a modern take on that tradition, as they pop up across the web. While I wasn’t directly invited to join the conversation this time, I decided to jump in after seeing <a href="https://blog.pifferi.io/die-liste-meiner-default-apps-in-2025/">Oliver’s</a> take on the topic, which caught my interest.

– 📨 Mail Service: self-hosted <a href="https://github.com/mailcow/mailcow-dockerized">Mailcow</a>
– 📮 Mail Client: <a href="https://www.thunderbird.net/">Thunderbird</a> (macOS), <a href="https://support.apple.com/en-gb/guide/mail/welcome/mac">Apple Mail</a> (iOS), <a href="https://www.sogo.nu">SOGo</a> (Web)
– 📝 Notizen: <a href="https://obsidian.md">Obsidian</a>
– ✅ To-Do: <a href="https://obsidian.md">Obsidian</a>
– 🌅 Fotoarchiv: <a href="https://www.icloud.com/photos?ref=blog.pifferi.io">iCloud Photos</a> / <a href="https://immich.app/?ref=blog.pifferi.io">Immich</a> (self-hosted)
– 📆 Kalender: <a href="https://support.apple.com/en-gb/guide/calendar/welcome/mac">Apple Calendar</a>
– 📁 Cloud-Speicher: <a href="https://nextcloud.com/?ref=blog.pifferi.io">Nextcloud</a>
– 📖 RSS-Dienst: <a href="https://freshrss.org/?ref=blog.pifferi.io">FreshRSS</a> (self-hosted)
– 🙍🏻‍♂️ Kontakte: <a href="https://support.apple.com/en-gb/guide/contacts/welcome/mac">Apple Contacts</a>
– 🌐 Browser: <a href="https://www.apple.com/safari/?ref=blog.pifferi.io">Safari</a> / <a href="https://brave.com/">Brave</a>
– 💬 Chat: preferred <a href="https://signal.org/?ref=blog.pifferi.io">Signal</a> &amp; <a href="https://support.apple.com/en-gb/guide/messages/welcome/mac">Messages</a> (also <a href="https://apps.apple.com/us/app/threema-the-secure-messenger/id578665578?ign-mpt=uo%3D4&amp;ref=blog.pifferi.io">Threema</a> &amp; <a href="https://www.whatsapp.com/">WhatsApp</a> 😔)
– 🛒 Einkaufslisten: <a href="https://support.apple.com/en-gb/guide/reminders/welcome/mac">Apple Reminders</a> &amp; <a href="https://obsidian.md">Obsidian</a>
– 🔎 Suche: <a href="https://sx.klein.ruhr">Whoogle</a> (self-hosted)
– 📚 Lesen: Kindle Paperwhite &amp; <a href="https://www.apple.com/de/apple-books/">Apple Books</a>
– 📰 Nachrichten: <a href="https://apps.apple.com/app/reeder-5/id1529445840?ref=blog.pifferi.io">Reeder Classic</a>
– 🎵 Musik: <a href="https://support.apple.com/en-gb/guide/music/welcome/mac">Apple Music</a>
– 🎤 Podcasts: <a href="https://support.apple.com/en-gb/guide/podcasts/welcome/mac">Apple Podcasts</a>
– 🌤️ Wetter: <a href="https://support.apple.com/en-gb/guide/weather-mac/welcome/mac">Apple Weather</a>
– 🔐 Passwortverwaltung: <a href="https://bitwarden.com/?ref=blog.pifferi.io">Bitwarden</a> (self-hosted)
– 🧮 Code-Editor: <a href="https://apps.apple.com/app/coteditor/id1024640650?mt=12&amp;ref=blog.pifferi.io">CotEditor</a></p>

<p>Inspired by Oliver, I’ve decided to make this list an annual tradition and plan to publish it regularly at the turn of the year. To ensure I don’t forget, I’ve already set a calendar reminder – because let’s be honest, without it, there’s almost no chance this would happen again. 🤪</p>

<p>The idea for this type of article originally came from <a href="https://rknight.me/">Robb Knights</a>, who continues to lead the “App Defaults” project. He curates and collects RSS feeds from people across the web, sharing which default apps and services they rely on.</p>

<p>If you enjoyed this article, why not join in? The concept practically begs for participation. 😉</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/list-of-my-default-apps-in-2025</guid>
      <pubDate>Thu, 23 Jan 2025 03:17:17 +0000</pubDate>
    </item>
    <item>
      <title>Setting up an internal domain</title>
      <link>https://blog.klein.ruhr/setting-up-an-internal-domain-with-nginx-proxy-manager-adguard-home-and-lets</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/settingupinternaldomain.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;Why Use an Internal Domain?&#xA;&#xA;A domain accessible only within the internal network can be incredibly useful when you host multiple services within your home lab. Not only does it provide a clean organization, but it also offers additional security since the services are not directly accessible from outside. In this article, I’ll show you how to set up an internal domain using an additional Nginx Proxy Manager (NPM) and an AdGuard Home Server within your home lab to structure and secure your self-hosted services.&#xA;&#xA;I wanted to replace my internal root CA with an accepted Let&#39;s Encrypt wildcard certificate, so I treated myself to an additional domain for internal use only. Like my external domain ‘klein.ruhr’, this is hosted by netcup and I use the Cloudflare.com name servers (only the pure DNS name servers, no additional services).&#xA;!--more--&#xA;Step-by-Step Guide&#xA;&#xA;1. Configuring the Second NPM&#xA;&#xA;1. Install Nginx Proxy Manager.&#xA;Here’s a simple example of a compose.yml file:&#xA;&#xA;   services:&#xA;  npm:&#xA;    image: jc21/nginx-proxy-manager&#xA;    ports:&#xA;      80:80&#xA;      443:443&#xA;      81:81 # Port 81 is for the NPM admin interface&#xA;    volumes:&#xA;      ./data:/data&#xA;      ./letsencrypt:/etc/letsencrypt&#xA;    restart: always&#xA;&#xA;2. Restrict access to internal networks:&#xA;&#xA;Ensure the NPM is only accessible from internal IPs, e.g., using firewall rules or container network settings.&#xA;&#xA;2. Obtaining SSL Certificates with Let’s Encrypt&#xA;&#xA;1. In the NPM interface, navigate to SSL-Certificates,  select “Add SSL Certificate” in the upper right corner.&#xA;&#xA;2. Mark “Use a DNS Challenge” and fill in the DNS Challenge information provided by your DNS provider.&#xA;&#xA;In the case of Cloudflare, log in to your Cloudflare Dashboard.&#xA;Go to My Profile → API Tokens and click Create Token.&#xA;Use the Edit Zone DNS template and configure it as follows:&#xA;   Zone Resources: Limit access to the specific domain (e.g., domain.example).&#xA;   Permissions: Set Zone → DNS → Edit.&#xA;Save the token and keep it in a secure location.&#xA;&#xA;3. Verify that the certificate is issued successfully.&#xA;&#xA;3. Setting up the correct routing with AdGuard Home&#xA;&#xA;1. Install AdGuard Home if not done so far (e.g., via Docker Compose).&#xA;Here’s a simple example of a compose.yml file:&#xA;&#xA;services:&#xA;  adguardhome:&#xA;    image: adguard/adguardhome&#xA;    ports:&#xA;      &#39;6060:6060/tcp&#39;&#xA;      &#39;5443:5443/udp&#39;&#xA;      &#39;5443:5443/tcp&#39;&#xA;      &#39;853:853/udp&#39;&#xA;      &#39;853:853/tcp&#39;&#xA;      &#39;3000:3000/tcp&#39; # Port 81 is for the AdGuardHome admin interface&#xA;      &#39;443:443/udp&#39;&#xA;      &#39;443:443/tcp&#39;&#xA;      &#39;80:80/tcp&#39;&#xA;      &#39;68:68/udp&#39;&#xA;      &#39;67:67/udp&#39;&#xA;      &#39;53:53/udp&#39;&#xA;      &#39;53:53/tcp&#39;&#xA;    volumes:&#xA;      ./conf:/opt/adguardhome/conf&#xA;      ./work:/opt/adguardhome/work&#xA;    restart: unless-stopped&#xA;    container_name: adguardhome&#xA;&#xA;2. Open your AdGuard Home, navigate to “Filters”, select “DNS rewrites”:&#xA;&#xA;3. add a simple DNS rewrite rule (adapt to your domain and IP address):&#xA;&#xA;4. Connecting Subdomains to Services&#xA;&#xA;1. Create an entry in NPM for each service:&#xA;&#xA;Subdomain: service1.domain.example.&#xA;Target: Internal service, e.g., http://192.168.x.y:port.&#xA;Test to ensure the routing works correctly.&#xA;&#xA;5. TroubleshootingIf issues arise, the following steps can help:&#xA;&#xA;1. DNS Errors:&#xA;&#xA;Ensure the DNS rewrite is correctly configured:&#xA;   run dig service1.domain.example or nslookup service1.domain.example to verify the IP.&#xA;If the rewrite doesn’t work, check your AdGuard Home configuration.&#xA;&#xA;2. SSL Certificate Issues:&#xA;&#xA;Verify that the DNS Challenge completed successfully:&#xA;In NPM: Go to Logs → SSL Certificates and check for errors.&#xA;Common cause: Incorrect API token or DNS entries.&#xA;&#xA;3. Access Problems:&#xA;&#xA;Ensure firewall rules and network settings are correct.&#xA;Check if the container is running and ports are open:&#xA;&#xA;docker ps&#xA;netstat -tuln | grep 81&#xA;&#xA;4. Routing Issues:&#xA;&#xA;Test the internal service directly via its IP and port to confirm it’s reachable.&#xA;Check the forwarding in the NPM logs:&#xA;NPM Interface   Logs   Proxy Hosts.&#xA;&#xA;Conclusion&#xA;&#xA;By setting up an internal domain, you ensure a more secure and efficient organization of your self-hosted services. This approach not only simplifies internal access but also integrates trusted SSL certificates, making it easier to maintain a professional and secure environment.]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/settingupinternaldomain.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;"></p>

<h2 id="why-use-an-internal-domain">Why Use an Internal Domain?</h2>

<p>A domain accessible only within the internal network can be incredibly useful when you host multiple services within your home lab. Not only does it provide a clean organization, but it also offers additional security since the services are not directly accessible from outside. In this article, I’ll show you how to set up an internal domain using an additional <a href="https://nginxproxymanager.com/">Nginx Proxy Manager</a> (NPM) and an <a href="https://github.com/AdguardTeam/AdGuardHome">AdGuard Home</a> Server within your home lab to structure and secure your self-hosted services.</p>

<p>I wanted to replace my internal root CA with an accepted <a href="https://letsencrypt.org/">Let&#39;s Encrypt</a> wildcard certificate, so I treated myself to an additional domain for internal use only. Like my external domain ‘klein.ruhr’, this is hosted by <a href="https://netcup.de">netcup</a> and I use the <a href="https://cloudflare.com">Cloudflare.com</a> name servers (only the pure DNS name servers, no additional services).
</p>

<h2 id="step-by-step-guide">Step-by-Step Guide</h2>

<h3 id="1-configuring-the-second-npm">1. Configuring the Second NPM</h3>

<p><strong>1.</strong> Install Nginx Proxy Manager.
Here’s a simple example of a <code>compose.yml</code> file:</p>

<pre><code class="language-yaml">services:
  npm:
    image: jc21/nginx-proxy-manager
    ports:
      - 80:80
      - 443:443
      - 81:81 # Port 81 is for the NPM admin interface
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    restart: always
</code></pre>

<p><strong>2.</strong> Restrict access to internal networks:</p>
<ul><li>Ensure the NPM is only accessible from internal IPs, e.g., using firewall rules or container network settings.</li></ul>

<h3 id="2-obtaining-ssl-certificates-with-let-s-encrypt">2. Obtaining SSL Certificates with Let’s Encrypt</h3>

<p><strong>1.</strong> In the NPM interface, navigate to SSL-Certificates,  select “Add SSL Certificate” in the upper right corner.</p>

<p><img src="https://data.klein.ruhr/images/npm1.webp" alt=""></p>

<p><strong>2.</strong> Mark “Use a DNS Challenge” and fill in the <strong>DNS Challenge</strong> information provided by your DNS provider.</p>
<ul><li>In the case of Cloudflare, log in to your <a href="https://dash.cloudflare.com/">Cloudflare Dashboard</a>.</li>
<li>Go to <strong>My Profile</strong> → <strong>API Tokens</strong> and click <strong>Create Token</strong>.</li>
<li>Use the <strong>Edit Zone DNS</strong> template and configure it as follows:
<ul><li><strong>Zone Resources</strong>: Limit access to the specific domain (e.g., domain.example).</li>
<li><strong>Permissions</strong>: Set Zone → DNS → Edit.</li></ul></li>
<li>Save the token and keep it in a secure location.</li></ul>

<p><img src="https://data.klein.ruhr/images/npm2.webp" alt=""></p>

<p><strong>3.</strong> Verify that the certificate is issued successfully.</p>

<p><img src="https://data.klein.ruhr/images/npm3.webp" alt=""></p>

<h3 id="3-setting-up-the-correct-routing-with-adguard-home">3. Setting up the correct routing with AdGuard Home</h3>

<p><strong>1.</strong> Install AdGuard Home if not done so far (e.g., via Docker Compose).
Here’s a simple example of a <code>compose.yml</code> file:</p>

<pre><code class="language-yaml">services:
  adguardhome:
    image: adguard/adguardhome
    ports:
      - &#39;6060:6060/tcp&#39;
      - &#39;5443:5443/udp&#39;
      - &#39;5443:5443/tcp&#39;
      - &#39;853:853/udp&#39;
      - &#39;853:853/tcp&#39;
      - &#39;3000:3000/tcp&#39; # Port 81 is for the AdGuardHome admin interface
      - &#39;443:443/udp&#39;
      - &#39;443:443/tcp&#39;
      - &#39;80:80/tcp&#39;
      - &#39;68:68/udp&#39;
      - &#39;67:67/udp&#39;
      - &#39;53:53/udp&#39;
      - &#39;53:53/tcp&#39;
    volumes:
      - ./conf:/opt/adguardhome/conf
      - ./work:/opt/adguardhome/work
    restart: unless-stopped
    container_name: adguardhome
</code></pre>

<p><strong>2.</strong> Open your AdGuard Home, navigate to “Filters”, select “DNS rewrites”:</p>

<p><img src="https://data.klein.ruhr/images/npm4.webp" alt=""></p>

<p><strong>3.</strong> add a simple DNS rewrite rule (adapt to your domain and IP address):</p>

<p><img src="https://data.klein.ruhr/images/npm5.webp" alt=""></p>

<h3 id="4-connecting-subdomains-to-services">4. Connecting Subdomains to Services</h3>

<p><strong>1.</strong> Create an entry in NPM for each service:</p>
<ul><li>Subdomain: service1.domain.example.</li>
<li>Target: Internal service, e.g., http://192.168.x.y:port.</li>
<li>Test to ensure the routing works correctly.</li></ul>

<p><img src="https://data.klein.ruhr/images/npm6.webp" alt=""></p>

<h3 id="5-troubleshooting-if-issues-arise-the-following-steps-can-help"><strong>5. Troubleshooting</strong>If issues arise, the following steps can help:</h3>

<p><strong>1.</strong> <strong>DNS Errors:</strong></p>
<ul><li>Ensure the DNS rewrite is correctly configured:
<ul><li>run <code>dig service1.domain.example</code> or <code>nslookup service1.domain.example</code> to verify the IP.</li></ul></li>
<li>If the rewrite doesn’t work, check your AdGuard Home configuration.</li></ul>

<p><strong>2.</strong> <strong>SSL Certificate Issues:</strong></p>
<ul><li>Verify that the DNS Challenge completed successfully:</li>
<li>In NPM: Go to <strong>Logs</strong> → <strong>SSL Certificates</strong> and check for errors.</li>
<li>Common cause: Incorrect API token or DNS entries.</li></ul>

<p><strong>3.</strong> <strong>Access Problems:</strong></p>
<ul><li>Ensure firewall rules and network settings are correct.</li>
<li>Check if the container is running and ports are open:</li></ul>

<pre><code>docker ps
netstat -tuln | grep 81
</code></pre>

<p><strong>4.</strong> <strong>Routing Issues:</strong></p>
<ul><li>Test the internal service directly via its IP and port to confirm it’s reachable.</li>
<li>Check the forwarding in the NPM logs:</li>
<li>NPM Interface &gt; <strong>Logs</strong> &gt; <strong>Proxy Hosts</strong>.</li></ul>

<h3 id="conclusion">Conclusion</h3>

<p>By setting up an internal domain, you ensure a more secure and efficient organization of your self-hosted services. This approach not only simplifies internal access but also integrates trusted SSL certificates, making it easier to maintain a professional and secure environment.</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/setting-up-an-internal-domain-with-nginx-proxy-manager-adguard-home-and-lets</guid>
      <pubDate>Mon, 30 Dec 2024 03:17:17 +0000</pubDate>
    </item>
    <item>
      <title>I switched from WordPress to Ghost</title>
      <link>https://blog.klein.ruhr/i-switched-from-wordpress-to-ghost</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/iswitchedfromghost.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;Recently, a controversy surrounding WordPress founder Matt Mullenweg has caused unrest within the community. &#xA;&#xA;At the center of the conflict is the hosting provider WP Engine, which Mullenweg accused of benefiting from the open-source nature of WordPress without contributing enough back to the project. This dispute escalated when Mullenweg demanded substantial financial compensation and threatened drastic measures. As a result, a significant portion of the workforce at Mullenweg’s company Automattic left the company.&#xA;&#xA;This development, along with the increasing commercialization of WordPress, led me to switch to Ghost. Ghost offers a powerful, streamlined, and privacy-friendly alternative, focusing on what really matters: publishing content.&#xA;!--more--&#xA;Advantages of Ghost over WordPress:&#xA;&#xA;Performance: Ghost is built from the ground up with speed in mind. While WordPress can become sluggish due to many plugins and complex themes, Ghost remains fast even with larger content volumes.&#xA;Content-focused: Ghost provides exactly what you need as a content creator, without unnecessary features. If your main focus is publishing articles and you don’t need extensive functionality, Ghost is the ideal choice.&#xA;GDPR compliance: Ghost is inherently more privacy-friendly than WordPress, which often requires additional plugins and adjustments to meet GDPR requirements. Especially in a self-hosted setup, Ghost gives you full control over your data.&#xA;Low maintenance: Without the burden of numerous plugins, which typically cause compatibility issues with WordPress, Ghost is easier to maintain. Updates usually run smoothly, and you face fewer technical conflicts.&#xA;&#xA;1. Installing and Setting up Ghost&#xA;&#xA;Installing Ghost via Docker-Compose gives you full control over your environment. While the process requires some technical knowledge, it rewards you with flexibility and easier management of your installation.&#xA;&#xA;Requirements&#xA;&#xA;A server or VM with shell access&#xA;Docker and Docker Compose installed&#xA;Access to your domain’s DNS settings&#xA;&#xA;Step-by-Step Guide&#xA;&#xA;Install Docker and Docker Compose: First, ensure that Docker and Docker Compose are installed on your server. Update your system and then run the installation:&#xA;&#xA;sudo apt update &amp;&amp; sudo apt upgrade -y&#xA;sudo apt install docker docker-compose -y&#xA;&#xA;Create the Docker-Compose file: Next, create a docker-compose.yml file to define the configuration for Ghost and the MySQL database. Add the following:&#xA;&#xA;services:&#xA;  ghost:&#xA;    image: ghost:latest&#xA;    ports:&#xA;      &#34;2368:2368&#34;&#xA;    volumes:&#xA;      ./ghostcontent:/var/lib/ghost/content&#xA;    environment:&#xA;      url: http://your-domain.com&#xA;  db:&#xA;    image: mysql:5.7&#xA;    environment:&#xA;      MYSQLROOTPASSWORD: yourpassword&#xA;      MYSQLDATABASE: ghost&#xA;      MYSQLUSER: ghost&#xA;      MYSQLPASSWORD: yourpassword&#xA;&#xA;Start the containers: Launch the containers using Docker Compose to set up Ghost and the database:&#xA;&#xA;docker compose up -d&#xA;&#xA;Set up domain and SSL: Point your domain to the server and enable SSL, for example, using Let&#39;s Encrypt to secure the connection.&#xA;Access the Ghost Dashboard: After Ghost is successfully installed and running, you can access the admin dashboard at http://your-domain.com/ghost to complete the setup and begin configuring your site.&#xA;&#xA;2. Differences in Using Ghost vs. WordPress&#xA;&#xA;Using Ghost differs in several key aspects from WordPress. While WordPress can feel bloated due to its many features and plugins, Ghost adopts a minimalist approach, focusing on creating and publishing content.&#xA;&#xA;User Interface&#xA;&#xA;Ghost offers a clean, minimalist interface that allows you to focus on writing. In contrast, WordPress’s interface can feel overwhelming with its additional features. Ghost also provides a live preview of your articles, letting you see how your post will look immediately. WordPress typically requires more clicks to achieve the same result.&#xA;&#xA;Themes and Design Customization&#xA;&#xA;Ghost has a smaller selection of themes, but they are fast and lightweight. Customizations often require basic HTML and CSS knowledge, which appeals to more tech-savvy users. WordPress offers countless themes and page builders like Elementor, but this variety often comes at the cost of performance.&#xA;&#xA;Plugins vs. Integrations&#xA;&#xA;One of the main differences lies in the expandability of the two systems. WordPress has a massive plugin ecosystem that covers nearly every function, but this often leads to security and performance issues. Ghost, on the other hand, relies on integrations with services like Zapier, Stripe, or Mailchimp, keeping the core system lean and stable.&#xA;&#xA;3. Content Management in Ghost&#xA;&#xA;Ghost offers a focused and fast content management system optimized for publishing articles and blog posts. Unlike WordPress, which provides many customization options and features, Ghost simplifies the content creation process.&#xA;&#xA;Creating Content&#xA;&#xA;Ghost uses a Markdown-based editor, which allows you to write content in a clean, structured way. The live preview shows you how your post will look in real-time. In comparison, WordPress uses the block-based Gutenberg editor, which is more flexible but can slow down your workflow.&#xA;&#xA;Categories and Tags&#xA;&#xA;While WordPress supports complex taxonomies such as categories, tags, and custom taxonomies, Ghost keeps it simple by only offering tags. This reduces complexity but is still sufficient for most content creators to organize their content.&#xA;&#xA;Media Embedding&#xA;&#xA;In Ghost, media is embedded directly in the editor without a central media library like WordPress. This makes the workflow straightforward, though WordPress offers more flexibility for managing large media collections.&#xA;&#xA;4. Plugins vs. Integrations&#xA;&#xA;A key difference between WordPress and Ghost is how each system extends its functionality. WordPress’s extensive plugin ecosystem provides maximum flexibility, but this comes with risks: security vulnerabilities, performance issues, and plugin conflicts are common.&#xA;&#xA;Ghost, by contrast, focuses on native integrations, keeping the core system lightweight and fast. Rather than relying on plugins, you can integrate Ghost with services like Zapier, Stripe, and Mailchimp for advanced functionality without compromising stability.&#xA;&#xA;Common Ghost Integrations:&#xA;&#xA;Zapier: Automate workflows and connect Ghost to over 1,000 services.&#xA;Stripe: Monetize your content with subscriptions and payments.&#xA;Mailchimp: Integrate your newsletter and stay connected with your readers.&#xA;Google Analytics: Track your page views without additional plugins.&#xA;&#xA;5. SEO and Performance&#xA;&#xA;For many site owners, search engine optimization (SEO) is crucial. Ghost provides many SEO features out of the box, whereas WordPress often relies on additional plugins like Yoast SEO.&#xA;&#xA;WordPress: Flexibility through Plugins&#xA;&#xA;WordPress users can install plugins like Yoast or RankMath for detailed control over meta tags, sitemaps, and more. These plugins offer a high degree of flexibility but can negatively impact performance and require regular maintenance.&#xA;&#xA;Ghost: SEO out of the box&#xA;&#xA;Ghost takes a minimalist approach, with many essential SEO features built-in. Automatically generated sitemaps, clean URLs, and meta tag management directly in the editor ensure a solid SEO foundation without the need for plugins. Additionally, Ghost supports native AMP (Accelerated Mobile Pages), improving load times on mobile devices and positively impacting search rankings.&#xA;&#xA;Performance Advantages of Ghost&#xA;&#xA;Because Ghost relies on clean code and minimal dependencies, it often loads much faster than WordPress, which can be slowed down by plugins and complex themes. Search engines like Google consider load speeds when ranking sites, giving Ghost an additional advantage in SEO optimization.&#xA;&#xA;6. Conclusion&#xA;&#xA;Switching from WordPress to Ghost depends heavily on your priorities. WordPress undoubtedly offers more flexibility with a vast array of plugins that cover almost every feature. However, this variety also brings challenges in terms of maintenance, performance, and privacy.&#xA;&#xA;Ghost is ideal for those seeking a lean, fast, and privacy-friendly solution. It focuses on the essentials: creating and publishing content. With built-in SEO features, native integrations, and a simple, user-friendly interface, Ghost is especially suited for content creators who value speed and simplicity.&#xA;&#xA;Ultimately, Ghost is an excellent alternative for those who prefer a minimalist and efficient CMS, while WordPress remains a solid choice for larger, more complex projects. Your decision should depend on whether you need a flexible platform with many extension options or a focused solution that offers better performance and less maintenance.]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/iswitchedfromghost.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
Recently, a controversy surrounding <a href="https://wordpress.org">WordPress</a> founder Matt Mullenweg has caused unrest within the community.</p>

<p>At the center of the conflict is the hosting provider <a href="https://wpengine.com/">WP Engine</a>, which Mullenweg accused of benefiting from the open-source nature of WordPress without contributing enough back to the project. This dispute escalated when Mullenweg demanded substantial financial compensation and threatened drastic measures. As a result, a significant portion of the workforce at Mullenweg’s company Automattic left the company.</p>

<p>This development, along with the increasing commercialization of WordPress, led me to switch to <a href="https://ghost.org">Ghost</a>. Ghost offers a powerful, streamlined, and privacy-friendly alternative, focusing on what really matters: publishing content.

<strong>Advantages of Ghost over WordPress:</strong></p>
<ul><li><strong>Performance</strong>: Ghost is built from the ground up with speed in mind. While WordPress can become sluggish due to many plugins and complex themes, Ghost remains fast even with larger content volumes.</li>
<li><strong>Content-focused</strong>: Ghost provides exactly what you need as a content creator, without unnecessary features. If your main focus is publishing articles and you don’t need extensive functionality, Ghost is the ideal choice.</li>
<li><strong>GDPR compliance</strong>: Ghost is inherently more privacy-friendly than WordPress, which often requires additional plugins and adjustments to meet GDPR requirements. Especially in a self-hosted setup, Ghost gives you full control over your data.</li>
<li><strong>Low maintenance</strong>: Without the burden of numerous plugins, which typically cause compatibility issues with WordPress, Ghost is easier to maintain. Updates usually run smoothly, and you face fewer technical conflicts.</li></ul>

<h2 id="1-installing-and-setting-up-ghost">1. Installing and Setting up Ghost</h2>

<p>Installing Ghost via Docker-Compose gives you full control over your environment. While the process requires some technical knowledge, it rewards you with flexibility and easier management of your installation.</p>

<h3 id="requirements">Requirements</h3>
<ul><li>A server or VM with shell access</li>
<li>Docker and Docker Compose installed</li>
<li>Access to your domain’s DNS settings</li></ul>

<h3 id="step-by-step-guide">Step-by-Step Guide</h3>
<ul><li><strong>Install Docker and Docker Compose</strong>: First, ensure that Docker and Docker Compose are installed on your server. Update your system and then run the installation:</li></ul>

<pre><code class="language-bash">sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install docker docker-compose -y
</code></pre>
<ul><li><strong>Create the Docker-Compose file</strong>: Next, create a <code>docker-compose.yml</code> file to define the configuration for Ghost and the MySQL database. Add the following:</li></ul>

<pre><code class="language-yaml">services:
  ghost:
    image: ghost:latest
    ports:
      - &#34;2368:2368&#34;
    volumes:
      - ./ghost_content:/var/lib/ghost/content
    environment:
      url: http://your-domain.com
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: yourpassword
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: yourpassword
</code></pre>
<ul><li><strong>Start the containers</strong>: Launch the containers using Docker Compose to set up Ghost and the database:</li></ul>

<pre><code class="language-bash">docker compose up -d
</code></pre>
<ul><li><strong>Set up domain and SSL</strong>: Point your domain to the server and enable SSL, for example, using Let&#39;s Encrypt to secure the connection.</li>
<li><strong>Access the Ghost Dashboard</strong>: After Ghost is successfully installed and running, you can access the admin dashboard at <code>http://your-domain.com/ghost</code> to complete the setup and begin configuring your site.</li></ul>

<h2 id="2-differences-in-using-ghost-vs-wordpress">2. Differences in Using Ghost vs. WordPress</h2>

<p>Using Ghost differs in several key aspects from WordPress. While WordPress can feel bloated due to its many features and plugins, Ghost adopts a minimalist approach, focusing on creating and publishing content.</p>

<h3 id="user-interface">User Interface</h3>

<p>Ghost offers a clean, minimalist interface that allows you to focus on writing. In contrast, WordPress’s interface can feel overwhelming with its additional features. Ghost also provides a live preview of your articles, letting you see how your post will look immediately. WordPress typically requires more clicks to achieve the same result.</p>

<h3 id="themes-and-design-customization">Themes and Design Customization</h3>

<p>Ghost has a smaller selection of themes, but they are fast and lightweight. Customizations often require basic HTML and CSS knowledge, which appeals to more tech-savvy users. WordPress offers countless themes and page builders like Elementor, but this variety often comes at the cost of performance.</p>

<h3 id="plugins-vs-integrations">Plugins vs. Integrations</h3>

<p>One of the main differences lies in the expandability of the two systems. WordPress has a massive plugin ecosystem that covers nearly every function, but this often leads to security and performance issues. Ghost, on the other hand, relies on integrations with services like Zapier, Stripe, or Mailchimp, keeping the core system lean and stable.</p>

<h2 id="3-content-management-in-ghost">3. Content Management in Ghost</h2>

<p>Ghost offers a focused and fast content management system optimized for publishing articles and blog posts. Unlike WordPress, which provides many customization options and features, Ghost simplifies the content creation process.</p>

<h3 id="creating-content">Creating Content</h3>

<p>Ghost uses a Markdown-based editor, which allows you to write content in a clean, structured way. The live preview shows you how your post will look in real-time. In comparison, WordPress uses the block-based Gutenberg editor, which is more flexible but can slow down your workflow.</p>

<h3 id="categories-and-tags">Categories and Tags</h3>

<p>While WordPress supports complex taxonomies such as categories, tags, and custom taxonomies, Ghost keeps it simple by only offering tags. This reduces complexity but is still sufficient for most content creators to organize their content.</p>

<h3 id="media-embedding">Media Embedding</h3>

<p>In Ghost, media is embedded directly in the editor without a central media library like WordPress. This makes the workflow straightforward, though WordPress offers more flexibility for managing large media collections.</p>

<h2 id="4-plugins-vs-integrations">4. Plugins vs. Integrations</h2>

<p>A key difference between WordPress and Ghost is how each system extends its functionality. WordPress’s extensive plugin ecosystem provides maximum flexibility, but this comes with risks: security vulnerabilities, performance issues, and plugin conflicts are common.</p>

<p>Ghost, by contrast, focuses on native integrations, keeping the core system lightweight and fast. Rather than relying on plugins, you can integrate Ghost with services like Zapier, Stripe, and Mailchimp for advanced functionality without compromising stability.</p>

<p><strong>Common Ghost Integrations:</strong></p>
<ul><li><strong>Zapier</strong>: Automate workflows and connect Ghost to over 1,000 services.</li>
<li><strong>Stripe</strong>: Monetize your content with subscriptions and payments.</li>
<li><strong>Mailchimp</strong>: Integrate your newsletter and stay connected with your readers.</li>
<li><strong>Google Analytics</strong>: Track your page views without additional plugins.</li></ul>

<h2 id="5-seo-and-performance">5. SEO and Performance</h2>

<p>For many site owners, search engine optimization (SEO) is crucial. Ghost provides many SEO features out of the box, whereas WordPress often relies on additional plugins like Yoast SEO.</p>

<h3 id="wordpress-flexibility-through-plugins">WordPress: Flexibility through Plugins</h3>

<p>WordPress users can install plugins like Yoast or RankMath for detailed control over meta tags, sitemaps, and more. These plugins offer a high degree of flexibility but can negatively impact performance and require regular maintenance.</p>

<h3 id="ghost-seo-out-of-the-box">Ghost: SEO out of the box</h3>

<p>Ghost takes a minimalist approach, with many essential SEO features built-in. Automatically generated sitemaps, clean URLs, and meta tag management directly in the editor ensure a solid SEO foundation without the need for plugins. Additionally, Ghost supports native AMP (Accelerated Mobile Pages), improving load times on mobile devices and positively impacting search rankings.</p>

<h3 id="performance-advantages-of-ghost">Performance Advantages of Ghost</h3>

<p>Because Ghost relies on clean code and minimal dependencies, it often loads much faster than WordPress, which can be slowed down by plugins and complex themes. Search engines like Google consider load speeds when ranking sites, giving Ghost an additional advantage in SEO optimization.</p>

<h2 id="6-conclusion">6. Conclusion</h2>

<p>Switching from WordPress to Ghost depends heavily on your priorities. WordPress undoubtedly offers more flexibility with a vast array of plugins that cover almost every feature. However, this variety also brings challenges in terms of maintenance, performance, and privacy.</p>

<p>Ghost is ideal for those seeking a lean, fast, and privacy-friendly solution. It focuses on the essentials: creating and publishing content. With built-in SEO features, native integrations, and a simple, user-friendly interface, Ghost is especially suited for content creators who value speed and simplicity.</p>

<p>Ultimately, Ghost is an excellent alternative for those who prefer a minimalist and efficient CMS, while WordPress remains a solid choice for larger, more complex projects. Your decision should depend on whether you need a flexible platform with many extension options or a focused solution that offers better performance and less maintenance.</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/i-switched-from-wordpress-to-ghost</guid>
      <pubDate>Mon, 21 Oct 2024 02:17:17 +0000</pubDate>
    </item>
    <item>
      <title>Ghost in compliance with the GDPR</title>
      <link>https://blog.klein.ruhr/ghost-in-compliance-with-the-gdpr-docker-compose</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/ghostgdpr.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;In the era of the GDPR (General Data Protection Regulation), it&#39;s essential for you to run your website in compliance with data protection regulations. Ghost is a modern, fast content management system that&#39;s gaining popularity in the self-hosting community. In this post, I&#39;ll guide you through how to run Ghost CMS with Docker-Compose while meeting GDPR requirements.&#xA;&#xA;Requirements and Setup&#xA;&#xA;Before you begin, make sure that Docker and Docker-Compose are installed on your server.&#xA;!--more--&#xA;Docker-Compose Setup for Ghost&#xA;&#xA;Here’s a simple docker-compose.yml file with the database and mail configurations already included:&#xA;&#xA;services:&#xA;  ghost:&#xA;    image: ghost:latest&#xA;    containername: ghost&#xA;    environment:&#xA;      url=https://your-domain.com&#xA;      databaseclient: mysql&#xA;      databaseconnectionhost: ghost-db&#xA;      databaseconnectionuser: ghost&#xA;      databaseconnectionpassword: &#34;replacewithsecurepassword&#34;&#xA;      databaseconnectiondatabase: ghost&#xA;      mailfrom: &#34;your.domain &#34;&#xA;      mailtransport: &#34;SMTP&#34;&#xA;      mailoptionshost: &#34;mail.your.domain&#34;&#xA;      mailoptionsport: &#34;465&#34;&#xA;      mailoptionssecureConnection: &#34;true&#34;&#xA;      mailoptionsauth_user: &#34;authuser@your.domain&#34;&#xA;      mailoptionsauth_pass: &#34;yoursmtppassword&#34;&#xA;    volumes:&#xA;      ./ghostdata:/var/lib/ghost/content&#xA;    ports:&#xA;      &#34;2368:2368&#34;&#xA;    restart: always&#xA;  db:&#xA;    image: mysql:latest&#xA;    containername: ghostdb&#xA;    environment:&#xA;      MYSQLROOTPASSWORD: replacewithsecurepassword&#xA;      MYSQLDATABASE: ghost&#xA;    volumes:&#xA;      ./ghostdb:/var/lib/mysql&#xA;    restart: always&#xA;&#xA;This is your starting point, but there are additional steps required for full GDPR compliance, particularly around data storage and user tracking.&#xA;&#xA;GDPR Compliance: Key Requirements for Operating Ghost CMS&#xA;&#xA;To ensure your Ghost CMS setup complies with the GDPR, make sure you follow these key guidelines:&#xA;&#xA;Data Storage: Any personal data (e.g., comments, newsletter sign-ups, contact forms) must be collected and processed only with explicit user consent.&#xA;SSL Encryption: A secure, encrypted connection (SSL) is crucial to protect your users&#39; data from unauthorized access.&#xA;Disable jsdelivr CDN: Instead of relying on the jsdelivr CDN, host all necessary files locally to maintain control over data transfers.&#xA;Tracking &amp; Analytics: Any third-party tracking should be fully transparent and require user consent. Consider using privacy-friendly alternatives to Google Analytics, or ensure correct implementation (such as anonymizing IP addresses).&#xA;Cookie Banner: If your site uses cookies, a clear notification that requires active user consent is mandatory.&#xA;&#xA;SSL Encryption&#xA;&#xA;I run all my services behind a proxy (Zoraxy), which handles SSL certificates via a wildcard certificate. Make sure to adjust this based on your own setup.&#xA;&#xA;Disabling jsdelivr CDN&#xA;&#xA;Identify the affected files&#xA;&#xA;In a typical Ghost installation using jsdelivr, the following files are loaded externally:&#xA;&#xA;sodo-search.min.js: JavaScript for the search function (depending on your theme)&#xA;sodo-search.main.css: CSS for the search function&#xA;portal.min.js: JavaScript for member and subscription functionality (Ghost Portal)&#xA;&#xA;Download files locally&#xA;&#xA;The first step is to download these files from jsdelivr and host them locally on your server. You can do this by visiting the respective jsdelivr URLs in your browser and downloading the files:&#xA;&#xA;sodo-search.min.js&#xA;sodo-search.main.css&#xA;portal.min.js&#xA;&#xA;Save these files in a directory within Ghost, such as /content/files/js for the JavaScript files and /content/files/css for the CSS files.&#xA;&#xA;Modify docker-compose.yml&#xA;&#xA;Now, add the following three lines to the environment section of your docker-compose.yml:&#xA;&#xA;      sodoSearchurl: &#34;/content/files/js/sodo-search.min.js&#34;&#xA;      sodoSearchstyles: &#34;/content/files/css/sodo-search.main.css&#34;&#xA;      portal_url: &#34;/content/files/js/portal.min.js&#34;&#xA;&#xA;Privacy Policy and Cookie Banner&#xA;&#xA;Privacy Policy: Ensure that your privacy policy covers all relevant points, including user data storage, tracking, and disclosure to third parties.&#xA;Cookie Banner: Use an open-source tool like CookieConsent to get user consent for cookies.&#xA;&#xA;GDPR-Friendly Analytics Tools&#xA;&#xA;While Google Analytics is widely used, it can be problematic from a data protection perspective. Luckily, there are alternatives like Umami, which is more privacy-friendly since it can be self-hosted and configured to anonymize users&#39; IP addresses.&#xA;&#xA;Conclusion&#xA;&#xA;Ghost CMS is a great platform for self-hosting, but making it GDPR-compliant requires a few extra steps. With the right configuration, SSL encryption, a clear cookie banner, and privacy-friendly analytics tools, you’re well on your way to running a fully GDPR-compliant Ghost site.&#xA;&#xA;  Tip: If you installed Ghost manually, you can find a German website here that describes the process for this type of installation.]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/ghostgdpr.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
In the era of the GDPR (General Data Protection Regulation), it&#39;s essential for you to run your website in compliance with data protection regulations. <a href="https://ghost.org">Ghost</a> is a modern, fast content management system that&#39;s gaining popularity in the self-hosting community. In this post, I&#39;ll guide you through how to run Ghost CMS with Docker-Compose while meeting GDPR requirements.</p>

<h3 id="requirements-and-setup">Requirements and Setup</h3>

<p>Before you begin, make sure that Docker and Docker-Compose are installed on your server.
</p>

<h3 id="docker-compose-setup-for-ghost">Docker-Compose Setup for Ghost</h3>

<p>Here’s a simple <code>docker-compose.yml</code> file with the database and mail configurations already included:</p>

<pre><code class="language-yaml">services:
  ghost:
    image: ghost:latest
    container_name: ghost
    environment:
      url=https://your-domain.com
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: ghost
      database__connection__password: &#34;replace_with_secure_password&#34;
      database__connection__database: ghost
      mail__from: &#34;your.domain &#34;
      mail__transport: &#34;SMTP&#34;
      mail__options__host: &#34;mail.your.domain&#34;
      mail__options__port: &#34;465&#34;
      mail__options__secureConnection: &#34;true&#34;
      mail__options__auth__user: &#34;auth_user@your.domain&#34;
      mail__options__auth__pass: &#34;your_smtp_password&#34;
    volumes:
      - ./ghost_data:/var/lib/ghost/content
    ports:
      - &#34;2368:2368&#34;
    restart: always
  db:
    image: mysql:latest
    container_name: ghost_db
    environment:
      MYSQL_ROOT_PASSWORD: replace_with_secure_password
      MYSQL_DATABASE: ghost
    volumes:
      - ./ghost_db:/var/lib/mysql
    restart: always
</code></pre>

<p>This is your starting point, but there are additional steps required for full GDPR compliance, particularly around data storage and user tracking.</p>

<h3 id="gdpr-compliance-key-requirements-for-operating-ghost-cms">GDPR Compliance: Key Requirements for Operating Ghost CMS</h3>

<p>To ensure your Ghost CMS setup complies with the GDPR, make sure you follow these key guidelines:</p>
<ul><li><strong>Data Storage</strong>: Any personal data (e.g., comments, newsletter sign-ups, contact forms) must be collected and processed only with explicit user consent.</li>
<li><strong>SSL Encryption</strong>: A secure, encrypted connection (SSL) is crucial to protect your users&#39; data from unauthorized access.</li>
<li><strong>Disable jsdelivr CDN</strong>: Instead of relying on the jsdelivr CDN, host all necessary files locally to maintain control over data transfers.</li>
<li><strong>Tracking &amp; Analytics</strong>: Any third-party tracking should be fully transparent and require user consent. Consider using privacy-friendly alternatives to Google Analytics, or ensure correct implementation (such as anonymizing IP addresses).</li>
<li><strong>Cookie Banner</strong>: If your site uses cookies, a clear notification that requires active user consent is mandatory.</li></ul>

<h3 id="ssl-encryption">SSL Encryption</h3>

<p>I run all my services behind a proxy (<a href="https://zoraxy.arozos.com/">Zoraxy</a>), which handles SSL certificates via a wildcard certificate. Make sure to adjust this based on your own setup.</p>

<h3 id="disabling-jsdelivr-cdn">Disabling jsdelivr CDN</h3>

<p><strong>Identify the affected files</strong></p>

<p>In a typical Ghost installation using jsdelivr, the following files are loaded externally:</p>
<ul><li><code>sodo-search.min.js</code>: JavaScript for the search function (depending on your theme)</li>
<li><code>sodo-search.main.css</code>: CSS for the search function</li>
<li><code>portal.min.js</code>: JavaScript for member and subscription functionality (Ghost Portal)</li></ul>

<p><strong>Download files locally</strong></p>

<p>The first step is to download these files from <a href="https://www.jsdelivr.com/package/npm/@tryghost/portal">jsdelivr</a> and host them locally on your server. You can do this by visiting the respective jsdelivr URLs in your browser and downloading the files:</p>
<ul><li><code>sodo-search.min.js</code></li>
<li><code>sodo-search.main.css</code></li>
<li><code>portal.min.js</code></li></ul>

<p>Save these files in a directory within Ghost, such as <code>/content/files/js</code> for the JavaScript files and <code>/content/files/css</code> for the CSS files.</p>

<p><strong>Modify docker-compose.yml</strong></p>

<p>Now, add the following three lines to the <code>environment</code> section of your <code>docker-compose.yml</code>:</p>

<pre><code class="language-bash">      sodoSearch__url: &#34;/content/files/js/sodo-search.min.js&#34;
      sodoSearch__styles: &#34;/content/files/css/sodo-search.main.css&#34;
      portal__url: &#34;/content/files/js/portal.min.js&#34;
</code></pre>

<h3 id="privacy-policy-and-cookie-banner">Privacy Policy and Cookie Banner</h3>
<ul><li><strong>Privacy Policy</strong>: Ensure that your privacy policy covers all relevant points, including user data storage, tracking, and disclosure to third parties.</li>
<li><strong>Cookie Banner</strong>: Use an open-source tool like <a href="https://www.cookieconsent.com/">CookieConsent</a> to get user consent for cookies.</li></ul>

<h3 id="gdpr-friendly-analytics-tools">GDPR-Friendly Analytics Tools</h3>

<p>While Google Analytics is widely used, it can be problematic from a data protection perspective. Luckily, there are alternatives like <a href="https://umami.is/">Umami</a>, which is more privacy-friendly since it can be self-hosted and configured to anonymize users&#39; IP addresses.</p>

<h3 id="conclusion">Conclusion</h3>

<p>Ghost CMS is a great platform for self-hosting, but making it GDPR-compliant requires a few extra steps. With the right configuration, SSL encryption, a clear cookie banner, and privacy-friendly analytics tools, you’re well on your way to running a fully GDPR-compliant Ghost site.</p>

<blockquote><p><strong>Tip:</strong> If you installed Ghost manually, you can find a German website <a href="https://dani.gg/de/ghost-dsgvo-konform-machen/">here</a> that describes the process for this type of installation.</p></blockquote>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/ghost-in-compliance-with-the-gdpr-docker-compose</guid>
      <pubDate>Fri, 18 Oct 2024 02:17:17 +0000</pubDate>
    </item>
    <item>
      <title>My software stack: tools I rely on</title>
      <link>https://blog.klein.ruhr/my-software-stack-tools-i-rely-on</link>
      <description>&lt;![CDATA[img src=&#34;https://data.klein.ruhr/images/toolsirelyon.webp&#34; alt=&#34;Beschreibung&#34; style=&#34;border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;&#34;&#xA;After introducing you to my daily companions in my homelab post, where I explained my Proxmox cluster and showcased other notable features of my #homelab, it’s time to dive into the software that powers my daily workflow.&#xA;&#xA;At work, I’m required to use Windows 11 - sometimes necessity dictates our tools. However, in my personal life, I gravitate toward macOS and always keep at least one Linux system running. My go-to distribution is usually #Debian-based, and right now, it’s Linux Mint.&#xA;&#xA;Since macOS is my primary operating system for personal use, this article will focus heavily on the software I use on macOS. Don’t worry if you’re not a Mac user - I rely on open-source software wherever possible, so many of these tools are available across multiple platforms.&#xA;&#xA;Here’s a rundown of the apps and software that keep my digital life organized and efficient:&#xA;!--more--&#xA;On My MacBook:&#xA;&#xA;Web Browsers:&#xA;&#xA;Mozilla Firefox: My preferred browser on all operating systems for its flexibility and privacy features.&#xA;Safari: A reliable alternative when I need a change or for specific tasks - it’s always good to have a backup.&#xA;&#xA;Email Clients:&#xA;&#xA;Thunderbird: My go-to email client, thanks to its versatility and robust features&#xA;&#xA;VPN:&#xA;&#xA;WireGuard: I self-host my own instance, making this lightweight and fast VPN my security partner.&#xA;&#xA;Cloud Solution:&#xA;&#xA;Nextcloud: More than just cloud storage, it’s an all-in-one tool that I self-host for privacy and control.&#xA;&#xA;Office Software:&#xA;&#xA;LibreOffice: A powerful and free open-source office suite that meets all my document needs.&#xA;&#xA;Password Manager:&#xA;&#xA;Bitwarden: I self-host my own Vaultwarden instance to manage passwords securely across all devices.&#xA;&#xA;Editor:&#xA;&#xA;cotEditor: A lightweight plain-text editor that’s perfect for quick edits on macOS.&#xA;&#xA;File Transfer Client:&#xA;&#xA;Cyberduck: A free, open-source file transfer client that supports a wide range of protocols, making file transfers a breeze.&#xA;Filezilla: FileZilla Client is a fast and reliable cross-platform FTP, FTPS and SFTP client with lots of useful features and an intuitive graphical user interface.&#xA;&#xA;Image Editing:&#xA;&#xA;GIMP: My open-source tool of choice for image editing, available on almost any operating system.&#xA;Pixelmator Pro: For when I need a more Mac-native alternative to Photoshop.&#xA;&#xA;Video Transcoding:&#xA;&#xA;HandBrake: The definitive free and open-source tool for all my video transcoding needs.&#xA;&#xA;Terminal:&#xA;&#xA;Ghostty: A fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration.&#xA;iTerm2: A powerful terminal emulator for macOS that enhances productivity with features like split panes and advanced search.&#xA;&#xA;Tools &amp; Helpers:- ICE: A menu bar management tool that keeps my workspace organized.&#xA;IINA: A modern and versatile media player tailored for macOS.&#xA;RustDesk: A self-hosted, full-featured remote control tool that’s my go-to for remote access.&#xA;&#xA;On My Server:&#xA;&#xA;Operating Systems:&#xA;&#xA;Debian 12.6 “bookworm”: A stable and reliable foundation for my server environment.&#xA;Ubuntu 24.04 LTS: A long-term support version that powers some of my critical services.&#xA;&#xA;Services:&#xA;For more details on my self-hosted apps, be sure to check out my separate blog post dedicated to that topic.&#xA;&#xA;Why These Tools?&#xA;&#xA;As you can see, my software choices reflect my philosophy of leveraging open-source tools and self-hosting wherever possible. I believe in the power of open-source not just for its flexibility, but also for the control and security it offers. By hosting services on my own infrastructure, I minimize reliance on third-party platforms and keep my data under my own control.&#xA;&#xA;Your Thoughts?&#xA;&#xA;What tools and software do you swear by in your own workflow? I’d love to hear your recommendations or thoughts in the comments below. Stay tuned for more insights on]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://data.klein.ruhr/images/toolsirelyon.webp" alt="Beschreibung" style="border-radius: 12px !important; box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important; margin: 2rem auto !important; display: block !important;">
After introducing you to my daily companions in my <a href="https://blog.klein.ruhr/homelab">homelab</a> post, where I explained my Proxmox cluster and showcased other notable features of my <a href="https://blog.klein.ruhr/tag:homelab" class="hashtag"><span>#</span><span class="p-category">homelab</span></a>, it’s time to dive into the software that powers my daily workflow.</p>

<p>At work, I’m required to use Windows 11 – sometimes necessity dictates our tools. However, in my personal life, I gravitate toward <a href="https://www.apple.com/macos/sonoma/">macOS</a> and always keep at least one Linux system running. My go-to distribution is usually <a href="https://blog.klein.ruhr/tag:Debian" class="hashtag"><span>#</span><span class="p-category">Debian</span></a>-based, and right now, it’s <a href="https://www.linuxmint.com/">Linux Mint</a>.</p>

<p>Since macOS is my primary operating system for personal use, this article will focus heavily on the software I use on macOS. Don’t worry if you’re not a Mac user – I rely on <a href="https://en.wikipedia.org/wiki/Open-source_software">open-source software</a> wherever possible, so many of these tools are available across multiple platforms.</p>

<p>Here’s a rundown of the apps and software that keep my digital life organized and efficient:
</p>

<h2 id="on-my-macbook">On My MacBook:</h2>

<h3 id="web-browsers">Web Browsers:</h3>
<ul><li><a href="https://www.mozilla.org/en-US/firefox/new/"><strong>Mozilla Firefox</strong></a>: My preferred browser on all operating systems for its flexibility and privacy features.</li>
<li><a href="https://www.apple.com/safari/"><strong>Safari</strong></a>: A reliable alternative when I need a change or for specific tasks – it’s always good to have a backup.</li></ul>

<h3 id="email-clients">Email Clients:</h3>
<ul><li><a href="https://www.thunderbird.net/en-US/"><strong>Thunderbird</strong></a>: My go-to email client, thanks to its versatility and robust features</li></ul>

<h3 id="vpn">VPN:</h3>
<ul><li><a href="http://wireguard.com"><strong>WireGuard</strong></a>: I self-host my own instance, making this lightweight and fast VPN my security partner.</li></ul>

<h3 id="cloud-solution">Cloud Solution:</h3>
<ul><li><a href="https://nextcloud.com"><strong>Nextcloud</strong></a>: More than just cloud storage, it’s an all-in-one tool that I self-host for privacy and control.</li></ul>

<h3 id="office-software">Office Software:</h3>
<ul><li><a href="https://www.libreoffice.org/"><strong>LibreOffice</strong></a>: A powerful and free open-source office suite that meets all my document needs.</li></ul>

<h3 id="password-manager">Password Manager:</h3>
<ul><li><a href="https://bitwarden.com/"><strong>Bitwarden</strong></a>: I self-host my own Vaultwarden instance to manage passwords securely across all devices.</li></ul>

<h3 id="editor">Editor:</h3>
<ul><li><a href="https://coteditor.com/"><strong>cotEditor</strong></a>: A lightweight plain-text editor that’s perfect for quick edits on macOS.</li></ul>

<h3 id="file-transfer-client">File Transfer Client:</h3>
<ul><li><a href="https://cyberduck.io/"><strong>Cyberduck</strong></a>: A free, open-source file transfer client that supports a wide range of protocols, making file transfers a breeze.</li>
<li><a href="https://filezilla-project.org"><strong>Filezilla</strong></a>: FileZilla Client is a fast and reliable cross-platform FTP, FTPS and SFTP client with lots of useful features and an intuitive graphical user interface.</li></ul>

<h3 id="image-editing">Image Editing:</h3>
<ul><li><a href="https://www.gimp.org/"><strong>GIMP</strong></a>: My open-source tool of choice for image editing, available on almost any operating system.</li>
<li><a href="https://www.pixelmator.com/pro/"><strong>Pixelmator Pro</strong></a>: For when I need a more Mac-native alternative to Photoshop.</li></ul>

<h3 id="video-transcoding">Video Transcoding:</h3>
<ul><li><a href="https://handbrake.fr/"><strong>HandBrake</strong></a>: The definitive free and open-source tool for all my video transcoding needs.</li></ul>

<h3 id="terminal">Terminal:</h3>
<ul><li><a href="https://ghostty.org"><strong>Ghostty</strong></a>: A fast, feature-rich, and cross-platform terminal emulator that uses platform-native UI and GPU acceleration.</li>
<li><a href="https://iterm2.com/"><strong>iTerm2</strong></a>: A powerful terminal emulator for macOS that enhances productivity with features like split panes and advanced search.</li></ul>

<h3 id="tools-helpers-ice-https-icemenubar-app-a-menu-bar-management-tool-that-keeps-my-workspace-organized">Tools &amp; Helpers:– <a href="https://icemenubar.app/"><strong>ICE</strong></a>: A menu bar management tool that keeps my workspace organized.</h3>
<ul><li><a href="https://iina.io/"><strong>IINA</strong></a>: A modern and versatile media player tailored for macOS.</li>
<li><a href="https://rustdesk.com/"><strong>RustDesk</strong></a>: A self-hosted, full-featured remote control tool that’s my go-to for remote access.</li></ul>

<h2 id="on-my-server">On My Server:</h2>

<h3 id="operating-systems">Operating Systems:</h3>
<ul><li><a href="https://www.debian.org/"><strong>Debian 12.6 “bookworm”</strong></a>: A stable and reliable foundation for my server environment.</li>
<li><a href="https://ubuntu.com/"><strong>Ubuntu 24.04 LTS</strong></a>: A long-term support version that powers some of my critical services.</li></ul>

<h3 id="services">Services:</h3>
<ul><li>For more details on my self-hosted apps, be sure to check out my separate <a href="https://blog.klein.ruhr/apps-i-selfhost/">blog post</a> dedicated to that topic.</li></ul>

<h2 id="why-these-tools">Why These Tools?</h2>

<p>As you can see, my software choices reflect my philosophy of leveraging open-source tools and self-hosting wherever possible. I believe in the power of open-source not just for its flexibility, but also for the control and security it offers. By hosting services on my own infrastructure, I minimize reliance on third-party platforms and keep my data under my own control.</p>

<h2 id="your-thoughts">Your Thoughts?</h2>

<p>What tools and software do you swear by in your own workflow? I’d love to hear your recommendations or thoughts in the comments below. Stay tuned for more insights on</p>
]]></content:encoded>
      <guid>https://blog.klein.ruhr/my-software-stack-tools-i-rely-on</guid>
      <pubDate>Sun, 06 Oct 2024 02:17:17 +0000</pubDate>
    </item>
  </channel>
</rss>