<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"><channel xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><title>IT Notes - snac</title><link>https://it-notes.dragas.net/categories/snac/</link><description>Articles in category snac</description><language>en</language><lastBuildDate>Mon, 25 May 2026 09:14:00 +0000</lastBuildDate><atom:link href="https://it-notes.dragas.net/categories/snac/feed.xml" rel="self" type="application/rss+xml"></atom:link><item><title>FediMeteo, timezones, and the art of not breaking what already works</title><link>https://it-notes.dragas.net/2026/05/25/fedimeteo-timezones-and-the-art-of-not-breaking-what-already-works/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/ZVhm6rEKEX8/download?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQwNTEzNjE5fA&amp;force=true&amp;w=640" alt="FediMeteo, timezones, and the art of not breaking what already works"&gt;&lt;/p&gt;&lt;p&gt;I have already written about &lt;a href="https://it-notes.dragas.net/2025/02/26/fedimeteo-how-a-tiny-freebsd-vps-became-a-global-weather-service-for-thousands/"&gt;how FediMeteo was born&lt;/a&gt;, and about how &lt;a href="https://it-notes.dragas.net/2026/05/18/fedimeteo-haproxy-and-the-art-of-not-wasting-snac-threads/"&gt;HAProxy helps reduce the number of requests that reach snac&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Seen from the outside, FediMeteo almost seems still. There is a static homepage, regenerated every hour. There are the city pages, with their forecasts. There are RSS feeds waiting to be fetched, JSON objects waiting to be requested, Fediverse instances refreshing data, subscribing, unsubscribing, retrieving profiles, and reading notes.&lt;/p&gt;
&lt;p&gt;That is the visible part.&lt;/p&gt;
&lt;p&gt;Behind it, however, &lt;a href="https://fedimeteo.com"&gt;FediMeteo&lt;/a&gt; is much more than a homepage, a few ActivityPub accounts, and a well-behaved reverse proxy. It is a chain of small pieces, in proper Unix style, each trying to do one thing and do it as well as possible.&lt;/p&gt;
&lt;p&gt;That chain, although almost invisible from the outside, was not born already tidy. It changed, was rewritten, adapted to new countries, timezones, ambiguous city names, external service limits, and also to my own mistakes.&lt;/p&gt;
&lt;p&gt;Some mistakes were small. Others were much less so.&lt;/p&gt;
&lt;p&gt;Because FediMeteo is a human project and, as such, imperfect. Imperfect in the way humans are imperfect, which today almost seems unfashionable. I like that.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The first version of the bot was almost embarrassingly simple, and I was proud of that.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It took a city name as input, asked &lt;a href="https://nominatim.org"&gt;Nominatim&lt;/a&gt; for the coordinates through &lt;code&gt;geopy&lt;/code&gt;, called the &lt;a href="https://open-meteo.com"&gt;Open-Meteo&lt;/a&gt; API for the current weather and the next several days, and printed a markdown block with current conditions, the forecast for today, the next twelve hours, and the coming days. The text was in Italian. The cities were Italian. The timezone was &lt;code&gt;Europe/Rome&lt;/code&gt;. There was nothing to calculate.&lt;/p&gt;
&lt;p&gt;Around the script, a small &lt;code&gt;sh&lt;/code&gt; wrapper read a list of cities and, for each one, ran the Python program and piped its output into &lt;code&gt;snac note_unlisted&lt;/code&gt;. A cron job ran the wrapper every six hours. The output was loose markdown, which snac happily renders, and the integration was: standard output goes into standard input. Nothing fancier than that.&lt;/p&gt;
&lt;p&gt;I like this kind of design. It is the part of the Unix philosophy that survives even when fashions change.&lt;/p&gt;
&lt;p&gt;When I started adding other European countries, I did not need to change much. I separated the operational logic from the localized strings, moved the strings into one JSON file per country, and spread the cron entries so that not every country posted in the same minute. Each country had its own snac instance, in its own FreeBSD jail, with its own dataset. The bot, internally, was almost the same script as before.&lt;/p&gt;
&lt;p&gt;This worked because Europe is, in essence, two or three timezones across most of the countries I cared about. &lt;/p&gt;
&lt;p&gt;Then I added Germany, and Germany taught me my first lesson about names.&lt;/p&gt;
&lt;p&gt;There are several places called Neustadt in Germany. There is a Frankfurt am Main, and a Frankfurt an der Oder, and they are not the same city. There is a Halle in Saxony-Anhalt and a Halle in North Rhine-Westphalia. Asking Nominatim for "Frankfurt, Germany" produced one of the two, consistently, but not always the one I wanted. Some German users wrote to me, politely, to point out that the forecast for "their" Frankfurt was, in fact, for the other one.&lt;/p&gt;
&lt;p&gt;I started thinking about disambiguation, but only enough to fix the immediate cases. The bot still took a single city name. The ambiguous ones I worked around by editing the cities file and hoping for the best.&lt;/p&gt;
&lt;p&gt;In hindsight, this was the seed of what would happen later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The United States broke every assumption the bot had grown up with&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The first problem was the number of cities. I wanted reasonable coverage at state level, which meant identifying the main cities for each of the fifty states. The list ended up at more than 1200 entries. That alone is more cities than every other country in the project combined.&lt;/p&gt;
&lt;p&gt;The second problem was timezones. The contiguous United States covers four of them, and Alaska and Hawaii bring the total to six. A "current weather at 12:00" line generated at the same instant for New York and for Los Angeles is technically the same instant, but the two cities are living different parts of the day, and the forecast for "today" is not even quite the same window. A bot that pretended every city was on the same clock would be wrong, sometimes embarrassingly so, every single day.&lt;/p&gt;
&lt;p&gt;The third problem was the name thing again, only larger. There are dozens of Springfields. There is a Portland in Oregon and a Portland in Maine. The Germany workaround - editing the cities file by hand and hoping Nominatim picked the right city - was clearly not going to scale to a country where the same name is also a state.&lt;/p&gt;
&lt;p&gt;I sat with this for a couple of days before admitting what I already knew.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The bot needed to be rewritten&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What made this hard was not the rewriting itself. It was the requirement to do it without breaking everything else.&lt;/p&gt;
&lt;p&gt;By the time I decided to add the United States, the infrastructure around the bot had grown into something I trusted. Jails, snapshots, backup jobs, cron schedules, snac instances on production paths, the HAProxy layer, the homepage cron that aggregated follower counts, and a long list of cities being processed in series every six hours. None of that knew or cared about the bot's internal shape. All of it cared, very much, about the bot's external behavior: a city name and a country code go in, valid markdown comes out, and that markdown ends up in a timeline.&lt;/p&gt;
&lt;p&gt;So the contract was clear, even if I had never written it down anywhere. The command-line interface, the output format, the exit codes, the way the wrapper script invoked it, the structure of the JSON country configs - all of it had to keep working. Italian had to keep working. German had to keep working. The cron job that ran every six hours had to keep producing the same shape of output, just with new countries added.&lt;/p&gt;
&lt;p&gt;What I changed was almost everything below the surface.&lt;/p&gt;
&lt;p&gt;The city argument grew an optional &lt;code&gt;__state&lt;/code&gt; suffix, with a double underscore as separator:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-text"&gt;python3 main.py springfield__illinois us
python3 main.py springfield__massachusetts us
python3 main.py new_york__new_york us
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A city without the suffix continued to work exactly as before, which is what every European country needed. The country config gained a &lt;code&gt;timezone&lt;/code&gt; field that could be a fixed string or the literal &lt;code&gt;"auto"&lt;/code&gt;; when it was &lt;code&gt;"auto"&lt;/code&gt;, the bot used &lt;code&gt;timezonefinder&lt;/code&gt; against the resolved coordinates to determine the right zone for that specific city. Internally I separated the weather provider behind an interface, so Open-Meteo could remain the primary while MET Norway and &lt;code&gt;wttr.in&lt;/code&gt; sat behind as alternatives, with automatic fallback when the primary failed. Units became configurable per country: temperature, wind speed, precipitation. The United States needed Fahrenheit, miles per hour, and inches. Most of Europe wanted Celsius, kilometers per hour, and millimeters. The bot now does either, on a per-country basis, without caring which is which.&lt;/p&gt;
&lt;p&gt;I am skipping a lot of small detail here, but the principle was always the same: every new degree of freedom had to be expressible as an optional field in the config or as an optional CLI flag. If a country did not set the new field, the old behavior continued, identical to before.&lt;/p&gt;
&lt;p&gt;I tested this by running the new bot against the old country configs and comparing the output line by line. Where it differed, it was a bug in the new bot. Not in the test.&lt;/p&gt;
&lt;p&gt;The first cycle after deploying the rewrite was, for every country except the United States, indistinguishable from the cycle before. That was the point.&lt;/p&gt;
&lt;p&gt;This is the part of the story I dislike telling, which is precisely why I should tell it.&lt;/p&gt;
&lt;p&gt;At some point during the development, while debugging an Open-Meteo response that did not look right, I added a &lt;code&gt;print&lt;/code&gt; statement to the error path that dumped the full request URL whenever something went wrong. The full URL of the Open-Meteo customer endpoint includes the &lt;code&gt;apikey&lt;/code&gt; query parameter. The print was meant for development. I forgot to remove it.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I deployed&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The next time Open-Meteo had an outage - and small ones happen, sometimes for several minutes at a time - the bot dutifully printed the failing request URL into the post body. For every city. For every cycle that ran during the outage. The wrapper script piped the output into &lt;code&gt;snac note_unlisted&lt;/code&gt; without complaint. The posts went out, federated across the Fediverse, with my API key sitting in the text for anyone who cared to read.&lt;/p&gt;
&lt;p&gt;Some users were kind enough to write me and tell me. Others were less kind, and made fun of me. Both groups were correct. This should not have happened.&lt;/p&gt;
&lt;p&gt;I reported the incident to the Open-Meteo team, who were extremely understanding. They rotated the key immediately and gave me a fresh one. I removed the debug print, and then I did the slightly more useful thing, which was to add redaction at multiple layers - in the bot's output, in the daemon's logging, and in the debug helpers themselves. URL query parameters that look like API keys are masked. Environment variables and config keys named &lt;code&gt;apikey&lt;/code&gt; or &lt;code&gt;OPEN_METEO_APIKEY&lt;/code&gt; are redacted before any string reaches stdout or a log file. Even JSON-like fields that include &lt;code&gt;open_meteo_apikey&lt;/code&gt; are scrubbed if they ever appear in something the program prints.&lt;/p&gt;
&lt;p&gt;The lesson is not "be more careful." The lesson is that debug paths leak, sooner or later, so the secrets have to be unreachable from the debug paths in the first place. Now they are.&lt;/p&gt;
&lt;p&gt;That afternoon, when I realised what was happening, I closed everything for a minute and looked out of the window. Then I started fixing.&lt;/p&gt;
&lt;p&gt;Nominatim is a public service, and it is generous, but it is not infinite. Every city in the project needs coordinates, and at the start of the project every cycle would re-ask Nominatim for every city. Most of the time this worked. Sometimes it did not.&lt;/p&gt;
&lt;p&gt;There was one cycle, before I added caching, when Nominatim simply did not respond for one of my queries. The geopy call timed out. The bot raised an exception. The wrapper script gave up on that city and moved on to the next one. A few users noticed that a particular city had not received its forecast that day, and asked what had happened.&lt;/p&gt;
&lt;p&gt;I added a coordinate cache, and I am still grateful that I did.&lt;/p&gt;
&lt;p&gt;The cache is intentionally boring. The first time the bot resolves a city, it writes the latitude and longitude into a small file under &lt;code&gt;/tmp&lt;/code&gt;, named after the city, and the state when present. Every subsequent run reads the file. If the file exists, no Nominatim call is made. If the file is missing, the bot calls Nominatim and writes the file. After the first successful lookup, the cache becomes the source of truth for the coordinates of that city.&lt;/p&gt;
&lt;p&gt;This is lighter on Nominatim, faster for every cycle, and much more resilient against transient failures. It is also nice for a reason I did not anticipate.&lt;/p&gt;
&lt;p&gt;Nominatim is a geocoder, and like every geocoder it has opinions.&lt;/p&gt;
&lt;p&gt;I live in Ferrara, so when I added Italy I made sure Ferrara was in the list, and I checked the first cycle to make sure everything looked right. The forecast came out fine. The temperature was reasonable. The icon matched the sky outside my window. I closed the laptop and forgot about it.&lt;/p&gt;
&lt;p&gt;Then, one evening months later, I looked more carefully at the coordinates Nominatim had returned for "Ferrara, Italy", and I realised they did not point to the city. They pointed to a location closer to the centroid of the &lt;em&gt;province&lt;/em&gt;, which is a much larger area and mostly countryside. The forecast had been, on average, for a field somewhere outside town, not for the city center.&lt;/p&gt;
&lt;p&gt;I am not entirely sure why I had not noticed earlier. Probably because the weather in Ferrara and the weather in the fields outside Ferrara is, on most days, indistinguishable to anyone who is not paying attention. But this is the kind of detail I do not want to leave wrong, especially for my own city.&lt;/p&gt;
&lt;p&gt;There are other places where geocoding lands slightly off. Sometimes it is a few kilometers, sometimes a different neighborhood, sometimes genuinely the wrong place.&lt;/p&gt;
&lt;p&gt;Because the cache is just a file per city, the fix is also just a file per city. I open the cache file, replace the latitude and longitude with the correct values, save. The next cycle uses the corrected coordinates. No code change, no redeploy, no special tooling. I keep a small list of patched cities in a separate text file, so that if I ever rebuild the cache, I do not lose the manual corrections.&lt;/p&gt;
&lt;p&gt;This is the kind of operational simplicity I like. A cache made of plain files costs almost nothing and quietly pays back every time a small problem appears.&lt;/p&gt;
&lt;p&gt;For every report it generates, the bot also writes a simplified English text snapshot to &lt;code&gt;/tmp/&amp;lt;city&amp;gt;.txt&lt;/code&gt;, or &lt;code&gt;/tmp/&amp;lt;city&amp;gt;__&amp;lt;state&amp;gt;.txt&lt;/code&gt; when there is a state.&lt;/p&gt;
&lt;p&gt;This is intentional, and it is not a debug artifact. I am not ready to say what I am doing with it yet, but it is part of a future direction for the project. Text is a useful intermediate format, and having a clean, language-neutral representation of every forecast sitting on disk costs almost nothing and might be worth a great deal later.&lt;/p&gt;
&lt;p&gt;I prefer to let ideas mature in private before I commit to them in public. So I will leave it at this for the moment.&lt;/p&gt;
&lt;p&gt;A full cycle for the United States takes hours.&lt;/p&gt;
&lt;p&gt;It is not because the work is heavy. It is because I deliberately inserted a small &lt;code&gt;sleep&lt;/code&gt; between cities, to give snac time to dispatch the previous post before the next one is generated. With more than 1200 cities in series, even a short pause adds up. I am not in a hurry. Forecasts that arrive a few minutes apart from each other are not a problem, and the bot was already a polite citizen elsewhere. A polite cycle is fine.&lt;/p&gt;
&lt;p&gt;The problem with a slow cycle is not the duration. The problem is what happens to it.&lt;/p&gt;
&lt;p&gt;In the original design, the cycle was launched by cron. Every six hours, cron called the wrapper script, the wrapper iterated through the cities file, and for each city it ran the bot and piped the output into snac. There was no scheduler in the project at all. Cron was the scheduler. The wrapper was just a loop.&lt;/p&gt;
&lt;p&gt;Restarting snac was harmless. The wrapper would call &lt;code&gt;snac note_unlisted&lt;/code&gt; per city, and if snac happened to be unavailable for a moment, that single call might fail, but the loop kept moving and snac was usually back within seconds. Snac itself was not what held the cycle together.&lt;/p&gt;
&lt;p&gt;What held the cycle together was the wrapper process. And the wrapper process lived inside the jail.&lt;/p&gt;
&lt;p&gt;If the FreeBSD jail was restarted while the wrapper was running, the loop stopped wherever it happened to be. The cron schedule did not care. Six hours later, the next cron tick started a new cycle from the first city, and the cities that had been about to be processed at the moment of the restart were simply skipped for that window. For the United States, this could mean several hundred cities going without an update.&lt;/p&gt;
&lt;p&gt;There was a worse case, and it took me longer than it should have to recognise it. If the host was rebooting &lt;em&gt;exactly&lt;/em&gt; in the minute when cron should have fired, cron simply did not fire. There was no daemon waiting to pick up the missed tick. The cycle never even started. Six hours of forecasts would be lost, in silence, with nothing in any log to suggest anything had gone wrong.&lt;/p&gt;
&lt;p&gt;I lived with this for a long time. Reboots were rare, the impact was limited, and adding state was the kind of thing I always meant to do "next week."&lt;/p&gt;
&lt;p&gt;What finally changed it was not a dramatic incident. It was the slow accumulation of small ones. A scheduled VPS reboot. A jail restart after an upgrade. Each one on its own was nothing. Together, they were a steady drip of missed cycles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So I wrote a daemon&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The crontab entries for the bot went away. There is now a long-running process inside the jail, started at boot, and it does the scheduling itself. The schedule is a list of hours and a minute, read from a JSON config. The daemon wakes up once a minute, checks whether it is time to start a cycle, and either starts one or waits.&lt;/p&gt;
&lt;p&gt;The interesting part is the state file.&lt;/p&gt;
&lt;p&gt;As the daemon walks through the cities file, it writes its position to a small JSON file: which cities file it is processing, and the index of the next city to handle. The write happens at the boundary between one city and the next, because that is the only place where resuming makes sense. If the daemon is interrupted mid-city, that city is retried on resume; no half-finished post escapes.&lt;/p&gt;
&lt;p&gt;When the daemon starts, it reads the state file. If it finds one matching the current cities file, it resumes from the saved index. If the cities file has changed since the state was written, the daemon starts fresh. The check is deliberately conservative: a renamed or modified cities file is treated as a different cycle, because the indices would otherwise be meaningless.&lt;/p&gt;
&lt;p&gt;The result is the behavior I should have had from the start. If the host reboots while the United States cycle is running, the daemon comes back up with the jail, reads the state, and continues from where it left off. Every city still gets its update, just with a small gap corresponding to the reboot itself. The cycle finishes. The state file is reset. Life goes on.&lt;/p&gt;
&lt;p&gt;And the worst case from the cron days is gone. The daemon does not need anyone to fire it. As long as the jail is running, the daemon is running, and the next scheduled cycle will happen when its hour comes, regardless of what was happening at any specific minute.&lt;/p&gt;
&lt;p&gt;Of all the changes I have made to the project, this is the one I like most. It is not exciting work. It is the kind of thing that earns no applause because, when it works, it produces no visible event. But it removes a whole class of small daily annoyances, and it makes a slow process robust against the boring kind of failure: the kind nobody plans for, but that always eventually happens.&lt;/p&gt;
&lt;p&gt;The current bot does considerably more than the original Italian script. It handles per-city timezones, three weather providers with automatic fallback, unit conversion for temperature, wind, and precipitation, optional air quality, pressure trend indicators when the provider supplies pressure data, a simplified English text snapshot for future use, a coordinate cache that can be patched by hand, secret redaction at multiple layers, a heartbeat that adapts to whichever HTTP client is installed on the host, and a scheduler-and-resume daemon that survives reboots.&lt;/p&gt;
&lt;p&gt;But from the outside, almost nothing has changed.&lt;/p&gt;
&lt;p&gt;The European country configs work the same way they always did. The wrapper scripts are unchanged. The snac integration is the same one-line pipe. The HAProxy layer in front does not know or care that the bot was rewritten. The homepage cron that counts followers and regenerates the static page works exactly as before.&lt;/p&gt;
&lt;p&gt;The original Italian script does not exist as a file anymore, but it survives as a default. A country config with &lt;code&gt;timezone&lt;/code&gt; set to &lt;code&gt;Europe/Rome&lt;/code&gt; and no special options behaves, today, exactly as the first version of the bot would have. Everything else is opt-in.&lt;/p&gt;
&lt;p&gt;I like this kind of work.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 25 May 2026 09:14:00 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2026/05/25/fedimeteo-timezones-and-the-art-of-not-breaking-what-already-works/</guid><category>server</category><category>networking</category><category>fediverse</category><category>snac</category><category>jail</category><category>ownyourdata</category><category>snac2</category><category>web</category><category>social</category><category>fedimeteo</category></item><item><title>FediMeteo, HAProxy, and the art of not wasting snac threads</title><link>https://it-notes.dragas.net/2026/05/18/fedimeteo-haproxy-and-the-art-of-not-wasting-snac-threads/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/ZVhm6rEKEX8/download?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQwNTEzNjE5fA&amp;force=true&amp;w=640" alt="FediMeteo, HAProxy, and the art of not wasting snac threads"&gt;&lt;/p&gt;&lt;p&gt;When I wrote about &lt;a href="https://it-notes.dragas.net/2025/02/26/fedimeteo-how-a-tiny-freebsd-vps-became-a-global-weather-service-for-thousands/"&gt;FediMeteo&lt;/a&gt; for the first time, I told the story from the beginning: the idea born almost by chance while checking the weather for a holiday, the memory of my grandfather, who for years had been my personal meteorologist, the decision to build something small and useful, and then the surprise of seeing people actually use it. What began as a personal experiment quickly became a small global service, still running with the same philosophy: FreeBSD, jails, simple scripts, snac, text, emoji, and a lot of small pieces doing their work quietly.&lt;/p&gt;
&lt;p&gt;That article was mostly about the birth and growth of the project. This one is about one of the less romantic parts of the same story, although I have to admit that I find a certain beauty in it too: keeping the service light as it grows.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://fedimeteo.com"&gt;FediMeteo&lt;/a&gt; is still intentionally simple from the outside. A homepage, some numbers, a list of countries, and many ActivityPub accounts publishing weather forecasts. The posts are text and emoji. There is no JavaScript requirement to read the pages, no heavy frontend, no unnecessary media attached to every forecast, and no dynamic homepage recalculated at every visit just to show the same numbers. This is not accidental. It is the way I wanted the service to behave from the beginning.&lt;/p&gt;
&lt;p&gt;But the more the service is used, the more the small details matter. A request that looks harmless when there are ten followers may become a repeated request when there are thousands of followers, remote instances, crawlers, previews, and other servers fetching the same public objects. In the Fediverse, the same small thing can be asked many times by many different places, each one with a perfectly legitimate reason. The backend doesn't care: it just needs to deal with the requests.&lt;/p&gt;
&lt;p&gt;And in FediMeteo, the backend is &lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I like snac very much precisely because it is small, clear, and efficient. It is not a giant application that tries to be everything. It does a focused job and does it well. But this also means that I want to respect its shape. I do not want to waste its threads on work that the reverse proxy can safely do. A snac thread serving the same public avatar again and again is not a tragedy, but it is still a waste. A snac thread answering the same public ActivityPub object several times in the same minute is doing real work, but often not necessary work.&lt;/p&gt;
&lt;p&gt;This is the reason behind the &lt;a href="https://www.haproxy.org"&gt;HAProxy&lt;/a&gt; tuning I am currently using in front of FediMeteo.&lt;/p&gt;
&lt;p&gt;It is not about making the configuration look clever. It is about keeping snac quiet.&lt;/p&gt;
&lt;h2&gt;A continuation of the same idea&lt;/h2&gt;
&lt;p&gt;I had already explored the same problem with snac and nginx in two previous posts:  &lt;a href="https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/"&gt;Improving snac Performance with Nginx Proxy Cache&lt;/a&gt;  and  &lt;a href="https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/"&gt;Caching snac Proxied Media with Nginx&lt;/a&gt;. In both cases, the idea was that the reverse proxy should absorb repeated public requests instead of letting them consume snac resources.&lt;/p&gt;
&lt;p&gt;This is especially important because snac uses a limited number of threads. I like that. Limits are healthy. They force us to understand what the service is doing, and they prevent a small program from pretending to be an infinite resource. But limits also make waste visible. If a few threads are busy serving files that could have been served from cache, those threads are not available for something more useful.&lt;/p&gt;
&lt;p&gt;With FediMeteo the implementation is different because the reverse proxy is HAProxy, but the reasoning is the same. I have many small snac instances, each one in its own FreeBSD (&lt;a href="https://github.com/BastilleBSD/bastille"&gt;Bastille&lt;/a&gt;) jail, and one public entry point that has to route, terminate TLS, compress, cache, and generally remove as much repetitive work as possible from the backends.&lt;/p&gt;
&lt;p&gt;This is, in a way, the natural continuation of the original FediMeteo design. In the first article I wrote that I wanted to manage everything according to the Unix philosophy: small pieces working together. This is another piece of that same puzzle. HAProxy does the edge work. snac does the ActivityPub work. Scripts generate forecasts. cron launches updates. ZFS gives me snapshots. FreeBSD jails keep countries separated. Nothing is particularly heroic by itself, but the whole system becomes pleasant because each part has a clear responsibility.&lt;/p&gt;
&lt;h2&gt;Why there is almost no media&lt;/h2&gt;
&lt;p&gt;Before talking about HAProxy, it is worth mentioning one of the most important optimizations, which is not in the proxy configuration at all.&lt;/p&gt;
&lt;p&gt;FediMeteo does not use media in its forecasts.&lt;/p&gt;
&lt;p&gt;No images attached to the posts, no generated weather cards, no maps for each city, no decorative banners. The forecasts are text and emoji. This was a deliberate decision. Weather information does not become more useful just because it is put inside an image, and every media file used by the service would become something to store, serve, cache, federate, expire, back up, and occasionally debug.&lt;/p&gt;
&lt;p&gt;Text and emoji are enough. They are accessible, light, readable in text browsers, friendly to timelines, and understandable even when someone does not know the local language perfectly. This was one of the original design principles of FediMeteo, and it also helps the infrastructure. Less media means less work, fewer cache entries, fewer repeated fetches, fewer surprises.&lt;/p&gt;
&lt;p&gt;There is one exception: the avatar.&lt;/p&gt;
&lt;p&gt;All FediMeteo accounts use the same avatar, and this is also intentional. I could have used a different avatar for each country, or for each city, or created something visually richer. It would have been nicer in some screenshots, perhaps. It would also have been operationally worse.&lt;/p&gt;
&lt;p&gt;With one shared avatar, the reverse proxy has one very useful object to cache. It is public, identical for everyone, small, requested often, and therefore almost always hot in cache. HAProxy can serve it directly instead of asking each snac instance to return the same file. Since avatars are requested by remote instances, browsers, profile previews, and all sorts of federation-related fetches, this single decision removes a surprising amount of pointless backend traffic.&lt;/p&gt;
&lt;p&gt;So the avatar is not only a visual identity. It is part of the architecture.&lt;/p&gt;
&lt;p&gt;This is the kind of optimization I like most, because it starts before the software. It starts with deciding not to create a problem.&lt;/p&gt;
&lt;h2&gt;The homepage is static because it can be static&lt;/h2&gt;
&lt;p&gt;The main homepage follows the same logic.&lt;/p&gt;
&lt;p&gt;It is a static HTML page generated from a template. Once per hour, a cron script updates the numbers and statistics. It counts the data I want to show, regenerates the page, and then the page remains static until the next run.&lt;/p&gt;
&lt;p&gt;This is not because I cannot make a dynamic page. It is because I do not need one. Boring is good.&lt;/p&gt;
&lt;p&gt;The homepage does not need to query all the country instances on every visit. It does not need a database request for each user who opens it. It does not need to ask snac anything in real time. The numbers are useful, but they do not need to be updated every second. Once per hour is enough, and it also fits the spirit of the whole project: do the work when it is needed, then serve the result cheaply.&lt;/p&gt;
&lt;p&gt;I have seen too many small services become heavy because the first implementation was convenient rather than appropriate. A cron job and a template are not fashionable, but they are often exactly what a page like this needs.&lt;/p&gt;
&lt;h2&gt;Many countries, one entry point&lt;/h2&gt;
&lt;p&gt;FediMeteo is made of many country instances. Each one runs in its own jail and listens on its own internal address and port. From the outside, however, they all live under the same domain structure:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-text"&gt;fedimeteo.com
www.fedimeteo.com
it.fedimeteo.com
uk.fedimeteo.com
jp.fedimeteo.com
us.fedimeteo.com
usa.fedimeteo.com
can.fedimeteo.com
canada.fedimeteo.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And many more.&lt;/p&gt;
&lt;p&gt;At the beginning, it is always tempting to write one ACL after another in the HAProxy frontend. It is quick, it is explicit, and for five hostnames it is perfectly fine. But FediMeteo did not remain at five hostnames. As countries and aliases grew, a long chain of ACLs would have turned the frontend into a list of names instead of a description of how the proxy behaves.&lt;/p&gt;
&lt;p&gt;So I moved the hostname to backend mapping into a map file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-text"&gt;fedimeteo.com        backend_fedimeteo
www.fedimeteo.com    backend_fedimeteo
it.fedimeteo.com     backend_it
uk.fedimeteo.com     backend_uk
jp.fedimeteo.com     backend_jp
us.fedimeteo.com     backend_us
usa.fedimeteo.com    backend_us
can.fedimeteo.com    backend_ca
canada.fedimeteo.com backend_ca
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The frontend then needs only one rule:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;use_backend %[req.hdr(host),field(1,:),lower,map(/usr/local/etc/fedimeteo.map,backend_fedimeteo)]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This reads the  &lt;code&gt;Host&lt;/code&gt;  header, removes the port if present, lowercases the result, and looks it up in  &lt;code&gt;/usr/local/etc/fedimeteo.map&lt;/code&gt;. If nothing matches, it falls back to the main FediMeteo backend.&lt;/p&gt;
&lt;p&gt;I like this because it keeps the configuration honest. The frontend contains the policy. The map contains the data. Adding a country means adding an entry to the map and defining a backend. I do not need to make the frontend more complicated every time the service grows.&lt;/p&gt;
&lt;h2&gt;Backends as small compartments&lt;/h2&gt;
&lt;p&gt;The country backends are deliberately plain:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;backend backend_it
    mode http
    http-reuse safe
    server srv1 10.0.0.2:8001 maxconn 30

backend backend_uk
    mode http
    http-reuse safe
    server srv1 10.0.0.7:8001 maxconn 30

backend backend_jp
    mode http
    http-reuse safe
    server srv1 10.0.0.32:8001 maxconn 30
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One backend, one jail, one snac instance. This is exactly the same organizational principle as the rest of the project. If I need to reason about Italy, I look at the Italian jail. If I need to reason about the United Kingdom, I look at the UK jail. If one day I need to move a country elsewhere, the separation is already there.&lt;/p&gt;
&lt;p&gt;The  &lt;code&gt;maxconn 30&lt;/code&gt;  value is not a magic number. It is a ceiling. I want each small backend to have a visible limit in front of it. If something starts hammering a country instance, I prefer the pressure to appear at the HAProxy layer instead of becoming unlimited concurrent work inside snac.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http-reuse safe&lt;/code&gt;  lets HAProxy reuse backend connections where appropriate. This is another small reduction in unnecessary work. Opening connections repeatedly is not the biggest problem in the world, but avoiding it is still better, especially when many small services sit behind the same proxy.&lt;/p&gt;
&lt;h2&gt;The front door&lt;/h2&gt;
&lt;p&gt;The HTTPS frontend listens on IPv4 and IPv6 and offers both HTTP/2 and HTTP/1.1:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;frontend https_in
    bind :::443 v4v6 ssl crt /usr/local/etc/certs/ alpn h2,http/1.1
    mode http
    option http-keep-alive
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;TLS defaults are set globally:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Port 80 only redirects to HTTPS, except for Let's Encrypt challenges:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;acl letsencrypt-acl path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 unless letsencrypt-acl
use_backend letsencrypt-backend if letsencrypt-acl
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the HTTPS frontend I also set the usual forwarding headers:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-request set-header X-Real-IP %[src]
http-request set-header X-Forwarded-Proto https
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And I add HSTS:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-response set-header Strict-Transport-Security &amp;quot;max-age=31536000; includeSubDomains; preload&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;None of this is unusual, and that is fine. The interesting parts of an infrastructure are not always the parts that should be unusual.&lt;/p&gt;
&lt;h2&gt;Two caches, because the requests are different&lt;/h2&gt;
&lt;p&gt;The HAProxy configuration defines two caches:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;cache mediacache
  total-max-size 128
  max-object-size 10000000
  max-age 3600
  process-vary on
  max-secondary-entries 12

cache jsoncache
  total-max-size 16
  max-object-size 1000000
  max-age 60
  process-vary on
  max-secondary-entries 12
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I keep media and ActivityPub JSON separate because they are not the same kind of traffic.&lt;/p&gt;
&lt;p&gt;The media cache is larger and has a longer maximum age. In FediMeteo, this mostly means the shared avatar and a few static-looking objects. Since there is intentionally almost no media, the important cached object is requested very often and remains warm.&lt;/p&gt;
&lt;p&gt;The JSON cache is smaller and short-lived. It is there for public ActivityPub GET requests, not to store federation state forever. A 60 second cache is enough to collapse many repeated requests that arrive close together in time, without pretending that ActivityPub responses should be treated like immutable files.&lt;/p&gt;
&lt;p&gt;This distinction is important. Caching is not one decision. It is a set of small decisions about what a response means, who can see it, how often it changes, and what happens if it is served again.&lt;/p&gt;
&lt;h2&gt;Recognizing media&lt;/h2&gt;
&lt;p&gt;For media, the ACL is based on file extensions:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;acl is_media path_end -i .jpg .jpeg .png .gif .webp .svg .ico .mp4 .webm .mp3 .ogg .wav .flac .mov .avi .mkv .m4v
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then I store the result in a transaction variable:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-request set-var(txn.is_media) bool(true) if is_media
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The cache lookup is straightforward:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-request cache-use mediacache if { var(txn.is_media) -m bool true }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And on the response side:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-response set-header Cache-Control &amp;quot;max-age=3600, public&amp;quot; if { var(txn.is_media) -m bool true }
http-response del-header Set-Cookie if { var(txn.is_media) -m bool true }
http-response del-header Vary if { var(txn.is_media) -m bool true }
http-response cache-store mediacache if { var(txn.is_media) -m bool true }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The  &lt;code&gt;Cache-Control&lt;/code&gt;  header makes the intent explicit.  &lt;code&gt;Set-Cookie&lt;/code&gt;  is removed because a public media object should not carry session information.  &lt;code&gt;Vary&lt;/code&gt;  is removed because I do not want the same avatar to fragment into many cache entries because of harmless header differences.&lt;/p&gt;
&lt;p&gt;This is aggressive only if removed from its context. In this service, with this media policy, it is a reasonable choice. FediMeteo is not serving private media under these paths. It is mostly serving the same public avatar over and over.&lt;/p&gt;
&lt;p&gt;For the same reason, I clean the request before it reaches the backend:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-request del-header Authorization if { var(txn.is_media) -m bool true }
http-request del-header Cookie        if { var(txn.is_media) -m bool true }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I would not do this globally. I do it after deciding that the request is media. Scope is what makes these rules safe.&lt;/p&gt;
&lt;p&gt;The result is exactly what I want: the shared avatar becomes an almost perfect cache object. Small, public, repeatedly requested, and served by HAProxy instead of snac.&lt;/p&gt;
&lt;h2&gt;ActivityPub JSON microcaching&lt;/h2&gt;
&lt;p&gt;The ActivityPub side starts from the  &lt;code&gt;Accept&lt;/code&gt;  header:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;acl is_ap_json   req.hdr(Accept),lower -m sub application/activity+json
acl is_ap_ldjson req.hdr(Accept),lower -m sub application/ld+json
acl is_outbox    path_end /outbox
acl is_get       method GET
acl has_auth     req.hdr(Authorization) -m found
acl has_cookie   req.hdr(Cookie) -m found
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This part matters because ActivityPub uses content negotiation. The same path may return HTML to a browser and JSON to a remote instance. If the proxy pretends that a URL is always one thing, it will eventually cache the wrong representation.&lt;/p&gt;
&lt;p&gt;So I only mark public ActivityPub GET requests as cacheable:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-request set-var(txn.is_activitypub) bool(true) if is_get !is_outbox is_ap_json !has_auth !has_cookie
http-request set-var(txn.is_activitypub) bool(true) if is_get !is_outbox is_ap_ldjson !has_auth !has_cookie
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There are several decisions here, all important.&lt;/p&gt;
&lt;p&gt;It must be a  &lt;code&gt;GET&lt;/code&gt;, because I am not caching deliveries or anything that changes state. It must not be  &lt;code&gt;/outbox&lt;/code&gt;, because outbox collections are not the traffic I want to cache here. It must not have  &lt;code&gt;Authorization&lt;/code&gt;, and it must not have cookies, because authenticated or user-specific requests do not belong in a shared public cache.&lt;/p&gt;
&lt;p&gt;Then the cache can be used and populated:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-request cache-use jsoncache if { var(txn.is_activitypub) -m bool true }

http-response set-header Cache-Control &amp;quot;max-age=60, public&amp;quot; if { var(txn.is_activitypub) -m bool true }
http-response cache-store jsoncache if { var(txn.is_activitypub) -m bool true }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sixty seconds is short, but useful. Federation often creates small clusters of identical requests. A remote server fetches an actor, another fetches the same actor, something asks for the same object, something retries. I do not need to cache these responses for hours. I only need HAProxy to answer the second and third identical request during the same small burst.&lt;/p&gt;
&lt;p&gt;This is microcaching in the most practical sense. It reduces repeated work without changing the nature of the service.&lt;/p&gt;
&lt;h2&gt;Static media paths&lt;/h2&gt;
&lt;p&gt;There is also a rule for static paths:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;acl is_short_path path_reg ^/[^/]+/s/
http-request cache-use mediacache if is_short_path
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This comes from the same observation that led me to cache snac media with nginx. snac uses static media paths, and those paths often represent the kind of public, repeatable traffic that should not consume backend threads if the proxy can serve it. I call them "short", not because they are, but because the first time I saw them, I thought the 's' stood for "short", not "static". The name just stuck.&lt;/p&gt;
&lt;p&gt;In FediMeteo this is less central than on a normal social instance, because I deliberately do not use media except for the avatar and basic static objects. Still, the rule fits the general policy: let HAProxy handle repeatable edge work, and let snac spend its threads where they are actually needed.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;Vary&lt;/code&gt;, but not without limits&lt;/h2&gt;
&lt;p&gt;Both caches have:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;process-vary on
max-secondary-entries 12
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I want HAProxy to process  &lt;code&gt;Vary&lt;/code&gt;, because content negotiation is real, especially when ActivityPub is involved. But I also want variation to be bounded. If every slightly different header creates another cache entry, the cache becomes a complicated way to miss.&lt;/p&gt;
&lt;p&gt;For media, I remove  &lt;code&gt;Vary&lt;/code&gt;  before storing the response. A shared avatar does not need to vary by  &lt;code&gt;Accept&lt;/code&gt;. For ActivityPub JSON, I am more careful because the representation matters.&lt;/p&gt;
&lt;p&gt;Again, the important thing is not the number itself. It is the decision to make variation explicit and limited.&lt;/p&gt;
&lt;h2&gt;Seeing whether it works&lt;/h2&gt;
&lt;p&gt;During rollout, I like to expose a very small diagnostic header:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;http-response set-header X-Cache-Status HIT if !{ srv_id -m found }
http-response set-header X-Cache-Status MISS if { srv_id -m found }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is intentionally simple. If HAProxy selected a backend server, I call it a miss. If no backend server was selected, the response came from cache, so I call it a hit. It is not a complete observability system, but it is enough to answer the first question I usually have after changing a cache rule.&lt;/p&gt;
&lt;p&gt;Did this request reach snac?&lt;/p&gt;
&lt;p&gt;A test can be as simple as:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;curl -I https://it.fedimeteo.com/path/to/avatar.png
curl -I https://it.fedimeteo.com/path/to/avatar.png
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The second request should be a hit.&lt;/p&gt;
&lt;p&gt;For ActivityPub JSON, the test must use the right  &lt;code&gt;Accept&lt;/code&gt;  header:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;curl -I \
  -H 'Accept: application/activity+json' \
  https://it.fedimeteo.com/some/activitypub/object
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And I also want to verify that cookies and authorization prevent public caching:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;curl -I \
  -H 'Cookie: test=value' \
  -H 'Accept: application/activity+json' \
  https://it.fedimeteo.com/some/activitypub/object

curl -I \
  -H 'Authorization: Bearer fake' \
  -H 'Accept: application/activity+json' \
  https://it.fedimeteo.com/some/activitypub/object
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A cache that works should be visible. A cache that is invisible can be correct, but it can also be silently wrong. I prefer to know.&lt;/p&gt;
&lt;h2&gt;Compression and operational paths&lt;/h2&gt;
&lt;p&gt;HAProxy also handles gzip compression:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;filter compression
compression algo gzip
compression type text/css text/html text/javascript application/javascript text/plain text/xml application/json application/activity+json
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This keeps another common responsibility at the edge. The country instances can stay focused on snac and the forecast data, while HAProxy deals with client-facing compression for HTML, JSON, and ActivityPub responses.&lt;/p&gt;
&lt;p&gt;There is also a local Prometheus exporter:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-haproxy"&gt;frontend prometheus
  bind 127.0.0.1:8405
  mode http
  http-request use-service prometheus-exporter
  no log
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And I keep internal operational paths, such as statistics and Grafana, handled before the hostname map. These are small details, but ordering matters. Special paths should be explicit and early. The hostname map is for FediMeteo routing, not for every internal tool I happen to expose behind the same proxy.&lt;/p&gt;
&lt;h2&gt;What this changes in practice&lt;/h2&gt;
&lt;p&gt;The nice thing about this configuration is that none of its parts is particularly surprising.&lt;/p&gt;
&lt;p&gt;The map keeps hostname routing manageable. The backend definitions keep each country isolated and limited. The static homepage avoids dynamic work for something that changes once per hour. The shared avatar gives HAProxy one very hot media object to serve directly. The media cache keeps public files away from snac. The JSON microcache absorbs short ActivityPub bursts. Header cleanup prevents useless variation. Connection reuse avoids unnecessary backend connection churn.&lt;/p&gt;
&lt;p&gt;But all of this is only a longer way of saying one thing:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;fewer requests reach snac&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;That is the metric I care about here.&lt;/p&gt;
&lt;p&gt;Not because snac is slow. If anything, FediMeteo exists in its current form because snac is efficient enough to make this kind of project possible on a very small VPS. But precisely because the whole architecture is small and pleasant, I do not want to waste resources where there is no need.&lt;/p&gt;
&lt;p&gt;This is also consistent with the rest of the project. Forecasts are serialized by scripts. Updates happen every six hours. The homepage is regenerated hourly. Countries live in separate jails. Snapshots and backups are handled outside the application. No single component tries to be the entire system.&lt;/p&gt;
&lt;p&gt;HAProxy is just another small piece, but it sits in the right place to remove a lot of repeated work.&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;This configuration is not a universal HAProxy recipe for ActivityPub services.&lt;/p&gt;
&lt;p&gt;It matches FediMeteo as it is now: almost no media, one shared avatar, static homepage, public forecasts, many small snac instances, and ActivityPub traffic that can benefit from a short public cache when there are no cookies or authorization headers.&lt;/p&gt;
&lt;p&gt;If I decide one day to use media in forecasts, the media cache rules will need to be reviewed. If I use different avatars for each city or country, the cache will still work, but I will lose the very nice property of one shared, always-hot avatar. If ActivityPub responses become actor-dependent, public JSON caching must be reconsidered. If one country grows a very different traffic pattern from the others, it may deserve a different limit or policy.&lt;/p&gt;
&lt;p&gt;This is why I do not like presenting configurations as magic. A good configuration is a written form of the assumptions behind a service. When the assumptions change, the configuration must change too.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;FediMeteo started as a small idea and became larger than I expected, but I still want it to feel small in the right ways. Small does not mean fragile. Small means understandable. It means that each part has a reason to exist, and that unnecessary work is removed before it becomes a problem.&lt;/p&gt;
&lt;p&gt;The HAProxy layer follows this idea. It terminates TLS, routes hostnames through a map, reuses backend connections, serves the shared avatar from cache, microcaches public ActivityPub JSON, avoids authenticated and cookie-based traffic, and gives me a small diagnostic header to see what is happening.&lt;/p&gt;
&lt;p&gt;There is no single brilliant directive here. There is only the usual work of matching infrastructure to reality.&lt;/p&gt;
&lt;p&gt;FediMeteo publishes weather forecasts as text and emoji. The homepage is static HTML updated every hour. The accounts share the same avatar because it is enough, and because it is better for the cache. Each country has its own snac instance in its own FreeBSD jail. HAProxy stands in front of them and tries, quietly, not to bother them unless it has to.&lt;/p&gt;
&lt;p&gt;I like this kind of infrastructure.&lt;/p&gt;
&lt;p&gt;Not because it is invisible, but because when it works well, it leaves very little to say.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Mon, 18 May 2026 09:44:00 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2026/05/18/fedimeteo-haproxy-and-the-art-of-not-wasting-snac-threads/</guid><category>freebsd</category><category>haproxy</category><category>server</category><category>networking</category><category>hosting</category><category>fediverse</category><category>snac</category><category>jail</category><category>ownyourdata</category><category>snac2</category><category>web</category><category>social</category><category>fedimeteo</category></item><item><title>FediMeteo: How a Tiny €4 FreeBSD VPS Became a Global Weather Service for Thousands</title><link>https://it-notes.dragas.net/2025/02/26/fedimeteo-how-a-tiny-freebsd-vps-became-a-global-weather-service-for-thousands/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/ZVhm6rEKEX8/download?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzQwNTEzNjE5fA&amp;force=true&amp;w=640" alt="FediMeteo: How a Tiny €4 FreeBSD VPS Became a Global Weather Service for Thousands"&gt;&lt;/p&gt;&lt;h2&gt;Personal Introduction&lt;/h2&gt;
&lt;p&gt;Weather has always significantly influenced my life. When I was a young athlete, knowing the forecast in advance would have allowed me to better plan my training sessions. As I grew older, I could choose whether to go to school on my motorcycle or, for safety reasons, have my grandfather drive me. And it was him, my grandfather, who was my go-to meteorologist. He followed all weather patterns and forecasts, a remnant of his childhood in the countryside and his life on the move. It's to him that I dedicate &lt;a href="https://fedimeteo.com"&gt;FediMeteo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The idea for &lt;a href="https://fedimeteo.com"&gt;FediMeteo&lt;/a&gt; started almost by chance while I was checking the holiday weather forecast to plan an outing. Suddenly, I thought how nice it would be to receive regular weather updates for my city directly in my timeline. After reflecting for a few minutes, I registered a domain and started planning.&lt;/p&gt;
&lt;h2&gt;Design Principles&lt;/h2&gt;
&lt;p&gt;The choice of operating system was almost automatic. The idea was to separate instances by country, and FreeBSD jails are one of the most useful tools for this purpose.&lt;/p&gt;
&lt;p&gt;I initially thought the project would generate little interest. I was wrong. After all, weather affects many of our lives, directly or indirectly. So I decided to structure everything in this way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I would use a test VPS to see how things would go. The VPS &lt;em&gt;was a small VM on a German provider with 4 shared cores, 4GB of RAM, 120GB of SSD disk space, and a 1Gbit/sec internet connection&lt;/em&gt; and now is a 4 euro per month VPS in Milano, Italy - 4 shared cores, 8 GB RAM and 75GB disk space.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I would separate various countries into different instances, for both management and security reasons, as well as to have the possibility of relocating just some of them if needed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Weather data would come from a reliable and open-source friendly source. I narrowed it down to two options: &lt;a href="https://wttr.in/"&gt;wttr.in&lt;/a&gt; and &lt;a href="https://open-meteo.com/"&gt;Open-Meteo&lt;/a&gt;, two solutions I know and that have always given me reliable results.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I would pay close attention to accessibility: forecasts would be in local languages, consultable via text browsers, with emojis to give an idea even to those who don't speak local languages, and everything would be accessible without JavaScript or other requirements. One's mother tongue is always more "familiar" than a second language, even if you're fluent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I would manage everything according to Unix philosophy: small pieces working together. The more years pass, the more I understand how valuable this approach is.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The software chosen to manage the instances is &lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt;. Snac embodies my philosophy of minimal and effective software, perfect for this purpose. It provides clear web pages for those who want to consult via the web, "speaks" the ActivityPub protocol perfectly, produces RSS feeds for each user (i.e., city), has extremely low RAM and CPU consumption, compiles in seconds, and is stable. The developer is an extremely helpful and positive person, and in my opinion, this carries equal weight as everything else.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I would do it for myself. If there was no interest, I would have kept it running anyway, without expanding it. So no anxiety or fear of failure.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Technical Implementation&lt;/h2&gt;
&lt;p&gt;I started setting up the first "pieces" during the days around Christmas 2024. The scheme was clear: each jail would handle everything internally. A Python script would download data, city by city, and produce markdown. The city coordinates would be calculated via the &lt;a href="https://geopy.readthedocs.io/en/stable/"&gt;geopy&lt;/a&gt; library and passed to &lt;a href="https://wttr.in/"&gt;wttr.in&lt;/a&gt; and &lt;a href="https://open-meteo.com/"&gt;Open-Meteo&lt;/a&gt;. No data would be stored locally. This approach gives the ability to process all cities together. Just pass the city and country to the script, and the markdown would be served. At that point, snac comes into play: without the need to use external utilities, the "snac note" command allows posting from stdin by specifying the instance directory and the user to post from. No need to make API calls with external utilities, having to manage API keys, permissions, etc.&lt;/p&gt;
&lt;h3&gt;Setting Up for Italy&lt;/h3&gt;
&lt;p&gt;To simplify things, I first structured the jail for Italy. I made a list of the main cities, normalizing them. For example, La Spezia became la_spezia. Forlì, with an accent, became forli - this for maximum compatibility since each city would be a snac user. I then created a script that takes this list and creates snac users via "snac adduser." At that point, after creating all the users, the script would modify the JSON of each user to convert the city name to uppercase, insert the bio (a standard text), activate the "bot" flag, and set the avatar, which was the same for all users at the time. This script is also able to add a new city: just run the script with the (normalized) name of the city, and it will add it - also adding it to the "cities.txt" file, so it will be updated in the next weather update cycle.&lt;/p&gt;
&lt;h3&gt;Core Application Development&lt;/h3&gt;
&lt;p&gt;I then created the heart of the service. A Python application (initially only in Italian, then multilingual, separating the operational part from the text) able to receive (via command line) the name of a city and a country code (corresponding to the file with texts in the local language). The script determines the coordinates and then, using API calls, requests the current weather conditions, those for the next 12 hours, and the next 7 days. I conducted experiments with both wttr.in and Open-Meteo, and both gave good results. However, I settled on Open-Meteo because, for my uses, it has always provided very reliable results. This application directly provides an output in Markdown since snac supports it, at least partially.&lt;/p&gt;
&lt;p&gt;The cities.txt file is also crucial for updates. I created a script - post.sh, in pure sh, that scrolls through all cities, and for each one, launches the FediMeteo application and publishes its output using snac directly via command line. Once the job is finished, it makes a call to my instance of &lt;a href="https://it-notes.dragas.net/2024/07/22/install-uptime-kuma-freebsd-jail/"&gt;Uptime-Kuma&lt;/a&gt;, which keeps an eye on the situation. In case of failure, the monitoring will alert me that there have been no recent updates, and I can check.&lt;/p&gt;
&lt;p&gt;At this point, the system cron takes care of launching post.sh every 6 hours. The requests are serialized, so the cities will update one at a time, and the posts will be sent to followers.&lt;/p&gt;
&lt;h2&gt;Growth and Unexpected Success&lt;/h2&gt;
&lt;p&gt;After listing all Italian provincial capitals, I started testing everything. It worked perfectly. Of course, I had to make some adjustments at all levels. For example, one of the problems encountered was that snac did not set the language of the posts, and some users could have missed them. The developer was very quick and, as soon as I exposed the problem, immediately modified the program so that the post could keep the system language, set as an environment variable in the sh script.&lt;/p&gt;
&lt;p&gt;After two days, I decided to start adding other countries and announce the project. And the announcement was unexpectedly well received: there were many boosts, and people started asking me to add their cities or countries. I tried to do what I could, within the limits of my physical condition, as in those days, I had the flu that kept me at home with a fever and illness for several days. I started adding many countries in the heart of Europe, translating the main indications into local languages but maintaining emojis so that everything would be understandable even to those who don't speak the local language. There were some small problems reported by some users. One of them: not all weather conditions had been translated, so sometimes they appeared in Italian - as well as errors. In bilingual countries, I tried to include all local languages. Sometimes, unfortunately, making mistakes as I encountered dynamics unknown to me or difficult to interpret. For example, in Ireland, forecasts were published in Irish, but it was pointed out to me that not everyone speaks it, so I modified and published in English.&lt;/p&gt;
&lt;h3&gt;A Turning Point&lt;/h3&gt;
&lt;p&gt;The turning point was when FediFollows (&lt;a href="https://social.growyourown.services/@FediFollows"&gt;@FediFollows@social.growyourown.services&lt;/a&gt; - who also manages the site &lt;a href="https://fedi.directory/"&gt;Fedi Directory&lt;/a&gt;) started publishing the list of countries and cities, highlighting the project. Many people became aware of FediMeteo and started following the various accounts, the various cities. And from here came requests to add new countries and some new information, such as wind speed. Moreover, I was asked (rightly, to avoid flooding timelines) to publish posts as unlisted - this way, followers would see the posts, but they wouldn't fill local timelines. Snac didn't support this, but again, the snac dev came to my rescue in a few hours.&lt;/p&gt;
&lt;h2&gt;Scaling Challenges&lt;/h2&gt;
&lt;p&gt;But with new countries came new challenges. For example, in my original implementation, all units of measurement were in metric/decimal/Celsius - and this doesn't adapt well to realities like the USA. Moreover, focusing on Europe, almost all countries were located in a single timezone, while for larger countries (such as Australia, USA, Canada, etc.), this is totally different. So I started developing a more complete and global version and, in the meantime, added almost all of Europe. The new version would have to be backward compatible, would have to take into account timezone differences for each city, different measurements (e.g., degrees C and F), as well as, initially more difficult part, being able to separate cities with the same name based on states or provinces. I had already seen a similar problem with the implementation of support for Germany, so it had to be addressed properly.&lt;/p&gt;
&lt;p&gt;The original goal was to have a VPS for each continent, but I soon realized that thanks to the quality of snac's code and FreeBSD's efficient management, even keeping countries in separate jails, the load didn't increase much. So I decided to challenge myself and the limits of the economical 4 euros per month VPS. That is, to insert as much as possible until seeing what the limits were. Limits that, to date, I have not yet reached. I would also soon exhaust the available API calls for Open-Meteo's free accounts, so I tried to contact the team and explain everything. I was positively surprised to read that they appreciated the project and provided me with a dedicated API key.&lt;/p&gt;
&lt;p&gt;Compatible with my free time, I managed to complete the richer and more complete version of my Python program. I'm not a professional dev, I'm more oriented towards systems, so the code is probably quite poor in the eyes of an expert dev. But, in the end, it just needs to take an input and give me an output. It's not a daemon, it's not a service that responds on the network. For that, snac takes care of it.&lt;/p&gt;
&lt;h2&gt;Expansion to North America&lt;/h2&gt;
&lt;p&gt;So I decided to start with a very important launch: the USA and Canada. A non-trivial part was identifying the main cities in order to cover, state by state, all the territory. In the end, I identified more than 1200 cities. A number that, by itself, exceeded the sum of all other countries (at that time). And the program, now, is able to take an input with a separator (two underscores: __) between city and state. In this way, it's possible to perfectly understand the differences between city and state: new_york__new_york is an example I like to make, but there are many.&lt;/p&gt;
&lt;p&gt;The launch of the USA was interesting: despite having had many previous requests, the reception was initially quite lukewarm, to my extreme surprise. The number of followers in Canada, in a few hours, far exceeded that of the USA. On the contrary, the country with the most followers (in a few days, more than 1000) was Germany. Followed by the UK - which I expected would have been the first.&lt;/p&gt;
&lt;h2&gt;System Performance&lt;/h2&gt;
&lt;p&gt;The VPS held up well. Except for the moments when FediFollows launched (after fixing some FreeBSD tuning, the service slowed slightly but didn't crash), the load remained extremely low. So I continued to expand: Japan, Australia, New Zealand, etc.&lt;/p&gt;
&lt;h2&gt;Current Status&lt;/h2&gt;
&lt;p&gt;At the time of the last update of this article (21 May 2026), the supported countries are 42: Argentina, Australia, Austria, Belgium, Brazil, Bulgaria, Canada, Cyprus, Croatia, Czechia, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, India, Indonesia, Ireland, Italy, Japan, Latvia, Lithuania, Malta, Mexico, Netherlands, New Zealand, Norway, Poland, Portugal, Romania, Slovakia, Slovenia, South Africa, Spain, Sweden, Switzerland, Taiwan, Turkey, the United Kingdom, and the United States of America (with more regions coming soon!).&lt;/p&gt;
&lt;p&gt;Direct followers in the Fediverse are around 8,889 and growing daily, excluding those who follow hashtags or cities via RSS, whose number I can't estimate. However, a quick look at the logs suggests there are many more.&lt;/p&gt;
&lt;p&gt;The cities currently covered are 3602 - growing based on new countries and requests.&lt;/p&gt;
&lt;h2&gt;Challenges Encountered&lt;/h2&gt;
&lt;p&gt;There have been some problems. The most serious, by my fault, was the API key leak: I had left a debug code active and, the first time Open-Meteo had problems, the error message also included the API call - including the API key. Some users reported it to me (others just mocked) and I fixed the code and immediately reported everything to the Open-Meteo team, who kindly gave me a new API Key and deactivated the old one.&lt;/p&gt;
&lt;p&gt;A further problem was related to geopy. It makes a call to Nominatim to determine coordinates. One of the times Nominatim didn't respond, my program wasn't able to determine the position and went into error. I solved this by introducing coordinate caching: now the program, the first time it encounters a city, requests and saves the coordinates. If present, they will be used in the future without making a new request via geopy. This is both lighter on their servers and faster and safer for us.&lt;/p&gt;
&lt;h2&gt;Infrastructure Details&lt;/h2&gt;
&lt;p&gt;And the VPS? It has no problems and is surprisingly fast and effective. FreeBSD 15.0-RELEASE, BastilleBSD to manage the jails. Currently, there are 43 jails - one for haproxy, the &lt;a href="https://fedimeteo.com"&gt;FediMeteo website&lt;/a&gt;, so nginx, and the snac instance for &lt;a href="https://fedimeteo.com/fedi/admin"&gt;FediMeteo announcements and support&lt;/a&gt; - the other 41 for the individual instances. Each of them, therefore, has its autonomous ZFS dataset. Every 15 minutes, there is a local snapshot of all datasets. Every hour, the homepage is regenerated: a small script calculates the number of followers (counting, instance by instance, the followers of individual cities, since I don't publish except in aggregate to avoid possible triangulations and privacy leaks of users). Every hour, moreover, an external backup is made via &lt;a href="https://it-notes.dragas.net/2022/05/30/how-we-are-migrating-many-of-our-servers-from-linux-to-freebsd-part-2/"&gt;zfs-autobackup&lt;/a&gt; (on encrypted at rest dataset), and once a day, a further backup is made in my datacenter, on disks encrypted with geli. The occupied RAM is 501 MB (yes, exactly: 501 MB), which rises slightly when updates are in progress. Updates normally occur every 6 hours. I have tried, as much as possible, to space them out to avoid overloads in timelines (or on the server itself). Only for the USA, I added a sleep of 5 seconds between one city and another, to give snac the opportunity to better organize the sending of messages. It probably wouldn't be necessary, with the current numbers, but better safe than sorry. In this way, the USA is processed in about 2 and a half hours, but the other jails (thus countries) can work autonomously and send their updates.&lt;/p&gt;
&lt;p&gt;The average load of the VPS (taking as reference both the last 24 hours and the last two weeks) is about 25%, as it rises to 70/75% when updates occur for larger instances (such as the USA), or when it is announced by FediFollows. Otherwise, it is on average less than 10%. So, the VPS still has huge margin, and new instances, with new nations, will still be inside it.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This article, although in some parts very conversational, aims to demonstrate how it's possible to build solid, valid, and efficient solutions without the need to use expensive and complex services. Moreover, this is the demonstration of how it's possible to have your online presence without the need to put your data in the hands of third parties or without necessarily having to resort to complex stacks. Sometimes, less is more.&lt;/p&gt;
&lt;p&gt;The success of this project demonstrates, once again, that my grandfather was right: weather forecasts interest everyone. He worried about my health and, thanks to his concerns, we spent time together. In the same way, I see many followers and friends talking to me or among themselves about the weather, their experiences, what happens. Again, in my life, weather forecasts have helped sociality and socialization.&lt;/p&gt;
&lt;p&gt;Thank you, Grandpa.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Wed, 26 Feb 2025 07:00:00 +0100</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/02/26/fedimeteo-how-a-tiny-freebsd-vps-became-a-global-weather-service-for-thousands/</guid><category>fediverse</category><category>snac</category><category>snac2</category><category>hosting</category><category>server</category><category>freebsd</category><category>networking</category><category>web</category><category>social</category><category>fedimeteo</category></item><item><title>Caching snac Proxied Media with Nginx</title><link>https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/9Xf-jxvfpW8/download?ixid=M3wxMjA3fDB8MXxhbGx8MXx8fHx8fHx8MTc0MTU5MTUzNXw&amp;force=true&amp;w=1920" alt="Photo by &amp;lt;a href=&amp;quot;https://unsplash.com/it/@elenarossini&amp;quot;&amp;gt;Elena Rossini&amp;lt;/a&amp;gt; on Unsplash"&gt;&lt;/p&gt;&lt;p&gt;One of the useful yet resource-intensive features of platforms like Mastodon is that they reprocess (and store locally) all multimedia files from other instances.&lt;/p&gt;
&lt;p&gt;This behavior is implemented for three valid reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To ensure files do not contain malicious code by reprocessing them locally.&lt;/li&gt;
&lt;li&gt;To prevent all users from all instances from overloading the original instance hosting the media by requesting it repeatedly.&lt;/li&gt;
&lt;li&gt;To hide the individual IP addresses of users from the original instance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While this approach has benefits, it also requires significant disk space - often many gigabytes per day.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt;, on the other hand, does not locally process or cache media by default. Instead, media URLs remain unchanged, meaning users will fetch content directly from its original source. This behavior is perfectly fine for many setups (such as mobile devices), but it may not be suitable for everyone.&lt;/p&gt;
&lt;p&gt;I suggested a small improvement to snac’s developer, who immediately recognized the benefit for users and implemented an interesting new feature: media proxying via the instance itself.&lt;/p&gt;
&lt;h3&gt;Enabling Media Proxying in snac&lt;/h3&gt;
&lt;p&gt;To enable this feature, simply add the following line to your &lt;code&gt;server.json&lt;/code&gt; configuration file:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-json"&gt;&amp;quot;proxy_media&amp;quot;: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once enabled, snac will rewrite all media URLs to pass through its own instance. This ensures that original instances will no longer see the IP addresses of individual users, as they will only see the IP of the snac instance itself.&lt;/p&gt;
&lt;p&gt;However, this also increases the load and bandwidth consumption of the instance since it must download and forward media every time a user accesses it.&lt;/p&gt;
&lt;h3&gt;Caching Proxied Media with nginx&lt;/h3&gt;
&lt;p&gt;To optimize performance, we can configure nginx to cache these proxied files transparently. This way, if multiple users from the snac instance (or the same user at different times) request the same media file, nginx will serve it from the local cache instead of fetching it again. This setup has two key advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reduces traffic&lt;/strong&gt; to the original instances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lowers the load&lt;/strong&gt; on the snac instance, as it won’t need to download the same file repeatedly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To achieve this, create a cache storage area in nginx by adding the following to the &lt;code&gt;http&lt;/code&gt; section:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;proxy_cache_path /var/cache/nginx/snac_media levels=1:2 keys_zone=snac_media:10m max_size=1g 
                 inactive=1d use_temp_path=off;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This defines a cache directory at &lt;code&gt;/var/cache/nginx/snac_media&lt;/code&gt;, with 10 MB allocated for metadata and a maximum cache size of 1 GB. Cached content will be invalidated and removed after one day, ensuring frequently accessed content (like profile avatars) remains cached while rarely used files get replaced over time.&lt;/p&gt;
&lt;p&gt;Next, add the following rule to your snac instance's virtual host configuration:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;location ~ ^/.+/(x|y)/ {
    proxy_cache snac_media;
    proxy_pass http://snac-ip:8001;
    proxy_set_header Host $host;
    proxy_cache_valid 200 1d;
    proxy_cache_valid 404 1h;
    proxy_ignore_headers &amp;quot;Cache-Control&amp;quot; &amp;quot;Expires&amp;quot;;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_lock on;
    add_header X-Proxy-Cache $upstream_cache_status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After reloading nginx, the cache will start populating as users request media files. Unrequested content will never enter the cache, ensuring efficient storage usage without unnecessary clutter.&lt;/p&gt;
&lt;p&gt;By combining snac’s new media proxying feature with nginx caching, we can achieve a more balanced setup—reducing load on both our instance and external ones, improving privacy, and enhancing performance.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Sat, 08 Feb 2025 16:00:00 +0100</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/</guid><category>snac</category><category>snac2</category><category>fediverse</category><category>nginx</category><category>data</category><category>hosting</category><category>server</category><category>tutorial</category><category>ownyourdata</category><category>networking</category><category>web</category><category>tipsandtricks</category><category>social</category></item><item><title>Improving snac Performance with Nginx Proxy Cache</title><link>https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/</link><description>&lt;p&gt;&lt;img src="https://it-notes.dragas.net/featured/web_text.webp" alt="Improving snac Performance with Nginx Proxy Cache"&gt;&lt;/p&gt;&lt;p&gt;Some days ago, I migrated my personal Fediverse instance from &lt;a href="https://akkoma.social/"&gt;Akkoma&lt;/a&gt; to &lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac&lt;/a&gt;. I appreciate snac a lot and believe it is the best solution available for many use cases.&lt;/p&gt;
&lt;p&gt;Akkoma is an excellent tool, but I noticed that even for a small instance like mine, the database grows exponentially, and the database activity remains constant. Despite low load, my disks are continuously "flashing" - which isn't a problem in itself but clearly indicates ongoing activity. In my case, this activity seems unnecessary since it's just a single-user instance.&lt;/p&gt;
&lt;p&gt;snac has shown excellent capabilities for managing the &lt;a href="https://fedimeteo.com"&gt;FediMeteo&lt;/a&gt; project (which I'll write about in detail soon), is lightweight, and has very few dependencies. Moreover, a dedicated snac instance handles sending updates from this blog to the Fediverse.&lt;/p&gt;
&lt;p&gt;After successfully transferring my followers from Akkoma to snac without major issues, I started using the new instance. However, as soon as I posted a photo (approximately 4MB), something happened that I somewhat expected but in a different form. My home internet connection (upload speed: 20 Mbit/sec) became saturated, but I also noticed that new connections and smaller entities were resulting in 499 errors - meaning nginx couldn't open new connections to snac. After some investigation, I realized the reason: for every remote instance, Nginx was requesting the multimedia file from snac. Due to saturated connections (snac allows setting the maximum number of active threads), it took several seconds, leading to thread exhaustion in snac. Consequently, subsequent nginx requests resulted in 499 errors as snac could no longer allocate a thread.&lt;/p&gt;
&lt;p&gt;To resolve this, I decided to implement direct caching using nginx. My reverse proxy (running in a different FreeBSD jail but this doesn't change the outcome) can cache multimedia files - storing them on first request and serving them directly to everyone who requests them without needing to ask snac every time. This approach is similar to &lt;a href="https://it-notes.dragas.net/2024/08/26/building-a-self-hosted-cdn-for-bsd-cafe-media/"&gt;what I use for the media in the BSD Cafe's Mastodon instance&lt;/a&gt;, where I employ &lt;a href="https://varnish-cache.org/"&gt;Varnish&lt;/a&gt; to keep everything in RAM.&lt;/p&gt;
&lt;p&gt;In snac, images and multimedia files are served from a specific path, such as: https://example.com/user/s/filename.png&lt;/p&gt;
&lt;p&gt;The key here is the &lt;em&gt;/s/&lt;/em&gt; segment. By instructing nginx to cache all files containing /s/ in their URL, I can offload some of the work from snac.&lt;/p&gt;
&lt;p&gt;I modified my &lt;code&gt;nginx.conf&lt;/code&gt; file to include a caching setup:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;# Caching configuration for snac
proxy_cache_path /var/cache/nginx/snac_cache levels=1:2 keys_zone=snac:10m max_size=1g inactive=1440m use_temp_path=off;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This creates a section for &lt;code&gt;/var/cache/nginx&lt;/code&gt; where I define the caching parameters. It will allocate 10 MB of RAM for metadata, with a maximum cache size of 1 GB (useful if you decide to post some videos). Cached content will be considered invalid and removed after 1440 minutes (24 hours). This ensures that frequently accessed content (like profile avatars and banners) remains in the cache while less frequently accessed content can be replaced with newer content. The goal isn't to have all content cached but rather for nginx to serve files independently during peak times, preventing multiple remote instances from overwhelming snac simultaneously.&lt;/p&gt;
&lt;p&gt;In the virtual host configuration for snac, I added this specific override for multimedia content:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-conf"&gt;# Caching rules for /s/ path
location ~ ^/.+/s/ {
    proxy_cache snac;
    proxy_pass http://snac-jail-ip:8001;
    proxy_set_header Host $host;
    proxy_cache_valid 200 1d;
    proxy_cache_valid 404 1h;
    proxy_ignore_headers &amp;quot;Cache-Control&amp;quot; &amp;quot;Expires&amp;quot;;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    proxy_cache_lock on;
    add_header X-Proxy-Cache $upstream_cache_status;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After restarting nginx, the multimedia files will be cached on their first access and served by nginx, leaving snac's threads free to handle everything else. This setup ensures smoother performance and prevents resource exhaustion during periods of high activity or when new content is shared.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Wed, 29 Jan 2025 09:00:00 +0100</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/</guid><category>snac</category><category>snac2</category><category>fediverse</category><category>nginx</category><category>data</category><category>hosting</category><category>server</category><category>freebsd</category><category>tutorial</category><category>ownyourdata</category><category>networking</category><category>web</category><category>tipsandtricks</category><category>social</category></item><item><title>Using a Permanent WebFinger Address for My Fediverse Profile</title><link>https://it-notes.dragas.net/2024/10/08/using-a-permanent-webfinger-address/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/9Xf-jxvfpW8/download?ixid=M3wxMjA3fDB8MXxhbGx8MXx8fHx8fHx8MTc0MTU5MTUzNXw&amp;force=true&amp;w=1920" alt="Photo by &amp;lt;a href=&amp;quot;https://unsplash.com/it/@elenarossini&amp;quot;&amp;gt;Elena Rossini&amp;lt;/a&amp;gt; on Unsplash"&gt;&lt;/p&gt;&lt;p&gt;Decentralized technologies have always been of great interest to me. In my opinion, the decentralization of the Internet was one of the keys to its success, but year after year, we are gradually losing it. This is happening because we increasingly rely on "mainstream" services for convenience, habit, or trend.&lt;/p&gt;
&lt;p&gt;I often hear colleagues say, "I've put my client's email on Y (Y = one of the major players in the email world) to avoid problems." The problems remain, but the colleague can "pass the buck" to the big player of the moment. And, in my experience, clients tend to be much more forgiving when the problem is with a big player.&lt;/p&gt;
&lt;p&gt;Once, people used to say: "Nobody ever got fired for buying IBM." Today, I would say, "Nobody ever got fired for putting email on [Google, Microsoft, etc.]."&lt;/p&gt;
&lt;p&gt;A few days ago, a colleague mentioned that he struggles to find some of his friends and colleagues on the fediverse because they are spread across various instances, and a simple search doesn't always yield the desired results.&lt;/p&gt;
&lt;h3&gt;Own Your Data&lt;/h3&gt;
&lt;p&gt;The fediverse is an extremely effective, resilient, autonomous decentralized communication technology. Unlike email, the many software implementations (such as Mastodon, GoToSocial, Mitra, Akkoma, Pleroma, etc. - soon snac will also support this type of operation) support the ability to "move" one's account. This means that when a user decides to relocate, the server will notify all followers that the user has moved. The followers will automatically stop following the original account and start following the new one. As of today, it is not possible to move past content (which will still be available on the old server).&lt;/p&gt;
&lt;p&gt;In my opinion, every government, public entity, association, foundation, etc., that needs to communicate with the public should have its own communication channel, with full control over its data and the messages it delivers. When I read "my Discord server," I feel like responding "there's nothing 'yours' about it - tomorrow morning they could shut everything down, and you would have lost EVERYTHING.". &lt;strong&gt;Own your data!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sometimes, I read that instances are not opened because of "costs not balanced by the number of users." But even public television channels are often economically unprofitable, yet they are considered an essential service for public communication. Open, decentralized technologies that ensure control over one's data should be treated the same way.&lt;/p&gt;
&lt;p&gt;The problem, as with email, is tied to the address. A server change necessarily implies an address change, which can sometimes cause problems.&lt;/p&gt;
&lt;h3&gt;A Permanent Address&lt;/h3&gt;
&lt;p&gt;For this reason, I decided to implement a kind of "permanent" address that will provide the correct data based on my main server. I decided that this address should be linked to my email address, and since I control the entire domain (including the web server), I can do this quite simply. In this way, if I change servers, users who already follow me will be moved by the "move" operation, while those who do not yet follow me will still be able to find me using my "permanent" address, which will, in turn, provide the new user on the new server.&lt;/p&gt;
&lt;p&gt;My setup will be very similar to what I found in &lt;a href="https://fnordig.de/2023/01/02/serving-webfinger-resources-with-nginx/"&gt;an interesting blog post&lt;/a&gt; that I based my approach on.&lt;/p&gt;
&lt;h3&gt;In Practice&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;My email address is &lt;code&gt;stefano@dragas.it&lt;/code&gt;, and I want anyone from any fediverse instance to be able to type it in and find me.&lt;/li&gt;
&lt;li&gt;My main account is currently on &lt;a href="https://bsd.cafe"&gt;BSD Cafe&lt;/a&gt;: &lt;a href="https://mastodon.bsd.cafe/@stefano"&gt;https://mastodon.bsd.cafe/@stefano&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To achieve this, I modified the nginx configuration on the server that serves the "dragas.it" website.&lt;/p&gt;
&lt;h3&gt;Nginx Configuration&lt;/h3&gt;
&lt;p&gt;Before the &lt;code&gt;server&lt;/code&gt; directive for dragas.it, I added this mapping:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-nginx"&gt;map $query_string $account_name {
    ~resource=acct:stefano@dragas.it$ stefano;
    ~resource=acct%3Astefano%40dragas.it$ stefano;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This ensures that requests for those two resources (some clients send the second one, so I wanted to handle that situation) will assign the value "stefano" to the &lt;code&gt;$account_name&lt;/code&gt; variable.&lt;/p&gt;
&lt;h3&gt;WebFinger Exception&lt;/h3&gt;
&lt;p&gt;Within the &lt;code&gt;server&lt;/code&gt; directive of the dragas.it virtual host, I added an exception for webfinger (the protocol used to find user data):&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-nginx"&gt;location = /.well-known/webfinger {
    root  /usr/local/www/webfinger;

    if ($account_name) {
      rewrite ^(.*)$ /$account_name.json break;
    }

    try_files $uri = 404;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;JSON File for WebFinger&lt;/h3&gt;
&lt;p&gt;At this point, I created the &lt;code&gt;/usr/local/www/webfinger&lt;/code&gt; directory and added a file named &lt;code&gt;stefano.json&lt;/code&gt; (matching &lt;code&gt;$account_name&lt;/code&gt;) with the following content:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-json"&gt;{
  &amp;quot;subject&amp;quot;: &amp;quot;acct:stefano@dragas.it&amp;quot;,
  &amp;quot;aliases&amp;quot;: [
    &amp;quot;https://mastodon.bsd.cafe/@stefano&amp;quot;,
    &amp;quot;https://mastodon.bsd.cafe/users/stefano&amp;quot;
  ],
  &amp;quot;links&amp;quot;: [
    {
      &amp;quot;rel&amp;quot;: &amp;quot;http://webfinger.net/rel/profile-page&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;text/html&amp;quot;,
      &amp;quot;href&amp;quot;: &amp;quot;https://mastodon.bsd.cafe/@stefano&amp;quot;
    },
    {
      &amp;quot;rel&amp;quot;: &amp;quot;self&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;application/activity+json&amp;quot;,
      &amp;quot;href&amp;quot;: &amp;quot;https://mastodon.bsd.cafe/users/stefano&amp;quot;
    },
    {
      &amp;quot;rel&amp;quot;: &amp;quot;http://ostatus.org/schema/1.0/subscribe&amp;quot;,
      &amp;quot;template&amp;quot;: &amp;quot;https://mastodon.bsd.cafe/authorize_interaction?uri={uri}&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In practice, this will return a reference to my main Mastodon account.&lt;/p&gt;
&lt;h3&gt;Testing the Setup&lt;/h3&gt;
&lt;p&gt;After reloading the nginx configurations, it will be enough to test it:&lt;/p&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh"&gt;# curl &amp;quot;https://dragas.it/.well-known/webfinger?resource=acct:stefano@dragas.it&amp;quot;

{
  &amp;quot;subject&amp;quot;: &amp;quot;acct:stefano@dragas.it&amp;quot;,
  &amp;quot;aliases&amp;quot;: [
    &amp;quot;https://mastodon.bsd.cafe/@stefano&amp;quot;,
    &amp;quot;https://mastodon.bsd.cafe/users/stefano&amp;quot;
  ],
  &amp;quot;links&amp;quot;: [
    {
      &amp;quot;rel&amp;quot;: &amp;quot;http://webfinger.net/rel/profile-page&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;text/html&amp;quot;,
      &amp;quot;href&amp;quot;: &amp;quot;https://mastodon.bsd.cafe/@stefano&amp;quot;
    },
    {
      &amp;quot;rel&amp;quot;: &amp;quot;self&amp;quot;,
      &amp;quot;type&amp;quot;: &amp;quot;application/activity+json&amp;quot;,
      &amp;quot;href&amp;quot;: &amp;quot;https://mastodon.bsd.cafe/users/stefano&amp;quot;
    },
    {
      &amp;quot;rel&amp;quot;: &amp;quot;http://ostatus.org/schema/1.0/subscribe&amp;quot;,
      &amp;quot;template&amp;quot;: &amp;quot;https://mastodon.bsd.cafe/authorize_interaction?uri={uri}&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this case, everything works perfectly. When you search for my email address from any fediverse instance, my &lt;a href="https://bsd.cafe"&gt;BSD Cafe&lt;/a&gt; profile will appear.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This approach allows you to define multiple users for each domain. You just need to add the corresponding mapping in the nginx configuration and the &lt;code&gt;.json&lt;/code&gt; file with all the data.&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Tue, 08 Oct 2024 12:53:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/10/08/using-a-permanent-webfinger-address/</guid><category>fediverse</category><category>ownyourdata</category><category>mastodon</category><category>snac2</category><category>snac</category><category>mitra</category><category>gotosocial</category><category>akkoma</category><category>hosting</category><category>social</category><category>web</category></item><item><title>A Small Compendium of Fediverse Platforms I Use</title><link>https://it-notes.dragas.net/2024/09/12/a-small-compendium-of-fediverse-platforms-i-use/</link><description>&lt;p&gt;&lt;img src="https://unsplash.com/photos/9Xf-jxvfpW8/download?ixid=M3wxMjA3fDB8MXxhbGx8MXx8fHx8fHx8MTc0MTU5MTUzNXw&amp;force=true&amp;w=1920" alt="Photo by &amp;lt;a href=&amp;quot;https://unsplash.com/it/@elenarossini&amp;quot;&amp;gt;Elena Rossini&amp;lt;/a&amp;gt; on Unsplash"&gt;&lt;/p&gt;&lt;p&gt;Following my &lt;a href="https://it-notes.dragas.net/2023/01/15/deploying-a-piece-of-the-fediverse/"&gt;old article about the Fediverse software I've experimented and use&lt;/a&gt;, I've decided to convert a Mastodon post into this small blog post.&lt;/p&gt;
&lt;p&gt;In the past few days, I revisited several of my old Fediverse instances after some friends asked me to help them set up a new one. While I was at it, I took the opportunity to perform maintenance on some leftover instances I still manage. Here’s a summary of my experience with various platforms:&lt;/p&gt;
&lt;h2&gt;&lt;a href="https://akkoma.social/"&gt;Akkoma&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is my oldest instance, still running since 2022. It was offline for about 3 or 4 months, but I recently updated it to the latest version and restarted it. After upgrading the software and the database, it didn't show any problem. Akkoma is a very good solution, supports quote posts and emoji reactions.&lt;/p&gt;
&lt;h2&gt;&lt;a href="https://gotosocial.org/"&gt;GoToSocial&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I helped a friend update their GoToSocial instance. While the software itself was up-to-date, the underlying system needed an update. I noticed that when the number of followings exceeds 2000, the instance becomes a bit sluggish. PostgreSQL isn’t the issue in this case – it's the GoToSocial process itself that seems to get heavy on the VPS. Despite this, GoToSocial remains very usable, and I see a lot of potential in it. The Mastodon API is well-implemented, and it works seamlessly with major apps.&lt;/p&gt;
&lt;h2&gt;&lt;a href="https://codeberg.org/silverpill/mitra"&gt;Mitra&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Mitra is another Fediverse platform I’ve been exploring. I helped someone with around 1000 followers and followings migrate from a large Mastodon instance to Mitra. There were no speed issues, though sending messages does make the server slightly "heavier" for a short time. The Mastodon API is partially implemented, but the software is evolving quickly. I find its native interface quite user-friendly, and it’s a platform worth keeping an eye on.&lt;/p&gt;
&lt;h2&gt;&lt;a href="https://codeberg.org/grunfink/snac2"&gt;Snac2&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’ve always had a soft spot for Snac2. It doesn’t use a database, and its design choices make it ideal for small instances. One feature I particularly like is how it sends posts to all known instances, which increases visibility and interaction. The interface is basic, with no JavaScript, which is a nice change, but it might feel too minimalistic for users coming from Mastodon. However, the Mastodon API support is steadily improving with each release. Snac2 does struggle with larger numbers, but this is more due to the underlying file system than the software itself. Snac2 now supports moving in/out, so it's easy top test it. I highly recommend it for anyone looking to self-host a small or single-user instance. &lt;/p&gt;
&lt;h2&gt;&lt;a href="https://github.com/mastodon/mastodon"&gt;Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My old personal instance of Mastodon was stuck on version 4.1.x and had been offline for a few months. I updated the FreeBSD Jail, upgraded Mastodon to 4.2.12, and then to 4.3.0 without any issues. I also helped a friend migrate their Pleroma-based instance to Mastodon. This user has about 5000 followers and followings, and the instance runs on FreeBSD on an arm64 VPS for around 3 euros per month. Aside from media storage (which isn’t Mastodon’s fault), there were no significant issues. Although Mastodon is sometimes criticized for being resource-intensive, its modular design ensures that even during high load, queues might slow down, but the local timeline and navigation remain reasonably fast. This makes it a strong contender for larger-scale use.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Overall, I feel that these platforms are evolving in the right direction. The developers are doing a fantastic job, and the Fediverse is growing stronger with each new release. Well done to all the devs working hard behind the scenes!&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Thu, 12 Sep 2024 18:45:00 +0200</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2024/09/12/a-small-compendium-of-fediverse-platforms-i-use/</guid><category>fediverse</category><category>akkoma</category><category>gotosocial</category><category>mitra</category><category>snac2</category><category>snac</category><category>mastodon</category><category>freebsd</category><category>hosting</category><category>server</category><category>social</category><category>web</category><category>ownyourdata</category></item><item><title>Deploying a piece of the Fediverse</title><link>https://it-notes.dragas.net/2023/01/15/deploying-a-piece-of-the-fediverse/</link><description>&lt;p&gt;&lt;img src="https://images.unsplash.com/photo-1456428746267-a1756408f782?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDEwNHx8c2VydmVyJTIwbmV0d29ya3xlbnwwfHx8fDE2NzM3NzQ3MDI&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Deploying a piece of the Fediverse"&gt;&lt;/p&gt;&lt;p&gt;After &lt;a href="https://en.wikipedia.org/wiki/Acquisition_of_Twitter_by_Elon_Musk"&gt;Elon Musk’s Twitter deal&lt;/a&gt;, many users &lt;a href="https://www.theverge.com/2022/12/20/23518325/mastodon-monthly-active-users-twitter-elon-musk"&gt;decided to “fly away” from the "traditional" commercial Social Networks&lt;/a&gt;. Some for good, some just decided to increase their presence in other, alternative Social Network. That's what I'm doing.&lt;/p&gt;
&lt;p&gt;Many of those users decided to join the &lt;a href="https://fediverse.info"&gt;Fediverse&lt;/a&gt; - even if many of them just call it &lt;a href="https://joinmastodon.org"&gt;Mastodon&lt;/a&gt;, as they don’t understand that Mastodon is just a Software that allows to join the Fediverse.&lt;/p&gt;
&lt;p&gt;The Fediverse is composed by thousands of “instances”, some are bigger (like &lt;a href="https://mastodon.social/explore"&gt;mastodon.social&lt;/a&gt;), some are personal (aka: single user instances), many are normal communities with their members. Many of them communicate using the same open protocol, &lt;a href="https://www.w3.org/TR/activitypub/"&gt;ActivityPub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Because of this, there’s no “one size fits all” so I’ve started to explore the different solutions. I’ve been mainly focusing on running them on &lt;a href="https://it-notes.dragas.net/2022/01/24/why-were-migrating-many-of-our-servers-from-linux-to-freebsd/"&gt;FreeBSD&lt;/a&gt;, but I’ve had to fire up Linux for some tests. Here’s what I’ve found out.&lt;/p&gt;
&lt;div class="hc-toc"&gt;&lt;/div&gt;

&lt;h2&gt;Backends / Complete Solutions&lt;/h2&gt;
&lt;h3&gt;&lt;a href="https://joinmastodon.org"&gt;Mastodon&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I won’t spend too much time on Mastodon as you may find almost everything,  everywhere, about it. Tons of articles have been written about Mastodon, so this one would be just another one, surely not the best one. Many consider it to be “the Fediverse” (they just say “Mastodon” to refer to the whole “Fediverse”, &lt;a href="https://blog.castopod.org/the-fediverse-is-so-much-bigger-than-mastodon/"&gt;and they’re wrong&lt;/a&gt;), it’s by far the most installed solution. It’s so popular that there are plenty of clients (both for Android and iOS) that perfectly work with it. Mastodon has its own APIs - and many other Fediverse solutions are using them, just to be able to be compatibile with the Mastodon apps. It also supports backend based “Hide replies” (only show new posts, not all the replies to other posts) in timeline. It’s good as you can filter the replies from any frontend or mobile app as the backend won’t provide them at all, while the client doesn't need to be aware that you're filtering them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am suggesting it&lt;/strong&gt;: It’s stable and well done, there’s a lot of documentation and if you install and manage it correctly, you shouldn’t notice anything strange or unexpected. Remember, no software solution can be considered “set and forget” and Mastodon is not an exception. Please, don’t forget that running an instance is not just installing the software. Moderation is a serious issue. More about it later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please, consider that&lt;/strong&gt;: Mastodon is also quite heavy, not easy to scale (&lt;a href="https://hazelweakly.me/blog/scaling-mastodon/"&gt;even if there’s documentation around&lt;/a&gt;) and, by default, is caching everything it sees and knows about. This means that both a single user instance or a thousands of users’ one, will (rapidly) grow because any media will be locally cached.&lt;/p&gt;
&lt;p&gt;My first, single user installation grew, in a week, well over 100 GB of occupied storage because of all this caching. It can’t be avoided (you can just tell Mastodon to delete the cache after &lt;em&gt;x&lt;/em&gt; days). While it may make sense for a big instance, it can be considered an overkill for a single user one. This is a "known problem", but mainly considered as a feature: all the media will be locally processed, all the contents will be locally stored. So an evil content hidden in a media file will be reprocessed by local ffmpeg or ImageMagick and will be cleared.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I’m suggesting another solution&lt;/strong&gt;: Monopoly is bad and Mastodon is becoming, for many, a synonym of Fediverse .  More, it requires much space and it’s resource-hungry. It’s easy to install Mastodon and experience a huge resource drain in just a few days, especially if you’re not a skilled system administrator. While Mastodon is the best solution for a complete microblogging experience, other solutions exist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To sum up&lt;/strong&gt;: Installing Mastodon on FreeBSD was easy. Even if I had read about problems, &lt;a href="https://it-notes.dragas.net/2022/11/23/installing-mastodon-on-a-freebsd-jail/"&gt;I’ve documented how to do it and it’s stable and reliable&lt;/a&gt;. Mastodon is, IMHO, a good piece of software but keep in mind that it could not be the best solution for you and a small, single user instance can become huge in a few weeks. Also, keep in mind that it's the most deployed Fediverse software, so any mobile app, any web app, any hint will work perfectly with Mastodon. Your Fediverse experience will be smooth.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://akkoma.social"&gt;Akkoma&lt;/a&gt; (&lt;a href="https://pleroma.social"&gt;Pleroma&lt;/a&gt; fork)&lt;/h3&gt;
&lt;p&gt;I’ve read about Akkoma in a reddit thread about how difficult was to install Mastodon on FreeBSD. It was described as a Pleroma fork, but actively developed and maintained, faster and with more advanced features. That’s why I decided to try it and - at least for now - stick with it (and not Pleroma, but many of the things I’ll point out here apply to Pleroma, too).&lt;/p&gt;
&lt;p&gt;Akkoma (as all the Pleroma forks) is much, much lighter than Mastodon. It’s perfectly able to run a single user instance on a Raspberry PI. Moreover, &lt;a href="https://www.linkedin.com/in/christine-lemmer-webber-aa8b93210?challengeId=AQEV7hPAP5kmZAAAAYW1APUoLUR1KExbqsA00X_acs1iXnaskmdkm-me-JY7qjRW2oqQlm6bvuKE7PaY88WTXasMsRZZx1lUTA&amp;amp;submissionId=d99bcdc8-2575-3a17-da9c-44bf4c03cb36&amp;amp;challengeSource=AgFktTMs4QFFzgAAAYW1ARkWNfsdRcZLCwaiJ0-pl_6jhKinHY94qCISxxZTZnM&amp;amp;challegeType=AgH1fssT9S9nagAAAYW1ARkZPK6hfooac1aWFgkA8hAdAMf0TNCthsU&amp;amp;memberId=AgHEvyepJMJAUwAAAYW1ARkbjUhwUbj0a-vmoG1ulu1a9kc&amp;amp;recognizeDevice=AgHmFlNf0hJLQgAAAYW1ARkedPChO-KJr-SC5ZM9P_7ylZB7LNDa"&gt;Christine Lemmer-Webber&lt;/a&gt;, coauthor of the &lt;a href="https://www.w3.org/TR/activitypub/"&gt;ActivityPub protocol&lt;/a&gt;, said that &lt;a href="https://octodon.social/@cwebber/109546851049168850"&gt;Akkoma is a good solution&lt;/a&gt; - and I think that her opinion is a qualified one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am suggesting it&lt;/strong&gt;: First of all, for its documentation. As for Pleroma, there are installation instructions for a lot of operating systems (yes, also &lt;a href="https://docs.akkoma.dev/stable/installation/freebsd_en/#installing-frontends"&gt;FreeBSD&lt;/a&gt;, &lt;a href="https://docs.akkoma.dev/stable/installation/netbsd_en/"&gt;NetBSD&lt;/a&gt;, &lt;a href="https://docs.akkoma.dev/stable/installation/openbsd_en/"&gt;OpenBSD&lt;/a&gt;). It’s light and fast. It doesn’t cache remote media by default, so that’s perfect for a single user instance. You can enable it (both pre-fetching media as soon as the server gets the status, like Mastodon, or just downloading them and caching when the first user meets them), you can also proxy your local media. S3 storage and remote CDNs are supported and everything is customisable. Message size limit is set to 5000 characters by default, but can be adjusted. Everything is configurable and you can choose your favourite frontend. It has quote posts (while Mastodon doesn’t allow them, even if they’re perfectly visible if created by Akkoma).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please, consider that&lt;/strong&gt;: Akkoma is not Mastodon. They “talk” using the same language but are different pieces of software. There’s less activity around it (the documentation is good, the support forum is good, but the number of Akkoma installations can’t be compared to Mastodon’s ones). Many Mastodon mobile apps seem to have problems with Akkoma and at the moment &lt;a href="https://meta.akkoma.dev/t/hashtags-from-akkoma-are-links-on-mastodon/"&gt;there’s a bug (probably it's a Mastodon bug, but users will think it's Akkoma's fault)  that, if you’re posting an hashtag, a link will be shown on Mastodon&lt;/a&gt;. More, if you want to hide boosts or replies from your timeline, remember that Akkoma won’t perform that at backend’s level, but it should be done by the frontend. Not all frontends and mobile apps support it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I’m suggesting another solution:&lt;/strong&gt; Well, not exactly. Actually, I’m suggesting to try Akkoma. It’s a good piece of software, developed by friendly people, well accepted by the Fediverse instances’ administrators and has a lot of happy users and instance administrators. I've used Akkoma as my main instance software for more or less one month. The bug I've described and some problems here and there made me move back to Mastodon. I'm keeping my Akkoma instance up, even if not actively used.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To sum up&lt;/strong&gt;: Akkoma installation is easy and well documented, you have a lot of settings to customise your instance and can fine-tune your installation for your hardware capabilities.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://join.misskey.page"&gt;Misskey&lt;/a&gt; (and its forks like &lt;a href="https://joinfirefish.org/"&gt;Firefish&lt;/a&gt;, etc.)&lt;/h3&gt;
&lt;p&gt;Misskey is a very nice piece of software. I had some troubles to run it on FreeBSD (but I didn’t try that much) so I decided to fire up a Linux machine and use Docker.&lt;/p&gt;
&lt;p&gt;The interface is nice - it’s Japanese design, so it’s fancy and rich of emojis and effects. I’ve tried it just for a few hours and I appreciated it but decided it wasn’t ok for my needs (at least, for now).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am suggesting it:&lt;/strong&gt; if you’re building a community, Misskey (or one of its forks) is a very good choice. It’s eye candy, complete and usable. It’s a part of the Fediverse, so no problems to talk to Mastodon, Akkoma, etc. It also has a “drive” feature, useful for many users that want to exchange files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I’m suggesting another solution:&lt;/strong&gt; the main reason why I had to look at another solution is that it doesn’t support Mastodon APIs so no Mastodon Android or iOS app is working with a Misskey instance. While it may be ok for many users, this could be a problem for others.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To sum up&lt;/strong&gt;: Misskey is nice, worth trying and a very good solution if it fits your community’s needs. For me, it doesn’t give any advantage over other lighter solutions.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://friendi.ca"&gt;Friendica&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Friendica is a project that aims to be similar to Facebook. It’s federated and actively maintained, the interface is nice and familiar to Facebook users. I’ve been able to install it on a FreeBSD jail (as it’s in PHP) and everything worked as expected. I didn’t spend too much time on Friendica but I’m planning to do a deeper test as I’m working on a community of former Facebook users and this could be the right choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am suggesting it:&lt;/strong&gt; If you’re creating a community for (former) Facebook users, they’ll have a familiar feeling in Friendica. The interface is clean and usable, it supports a lot of protocols (ActivityPub, OStatus, diaspora), it supports plugins, can import websites via rss (so automatic post is easy).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please, consider that&lt;/strong&gt;: Moderation tools are different from the ones you have on Mastodon or Akkoma (Pleroma, etc.) and you can’t easily report users, especially remote ones as support for Mastodon API is limited. With the current growth of users, it’s easy to find a bad person trying to disturb. Not having effective ways to deal with it may be frustrating.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I’m suggesting another solution:&lt;/strong&gt; I didn’t try Friendica long enough to find some big problems with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To sum up:&lt;/strong&gt; Generally speaking, it is considered a solid and stable solution, actively maintained and, being in php, portable. If you want to create a Facebook-like community, that's the way to go.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://codeberg.org/grunfink/snac2"&gt;snac2&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;snac2 is a simple, minimalistic ActivityPub instance that supports the Mastodon API. This makes it compatible with platforms like Pleroma, Akkoma, and Mastodon itself. It's written in portable C and it's been created to be light, easy to deploy, and with only two dependencies: &lt;em&gt;openssl&lt;/em&gt; and &lt;em&gt;curl&lt;/em&gt;. It heavily relies on hard links and &lt;em&gt;doesn't need any database&lt;/em&gt;. This is a big, big plus for me. I've performed many tests and found that this is one of the best lightweight solutions to join the Fediverse.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am suggesting it:&lt;/strong&gt; snac2 is clean and polished. The federation with the other solutions is good, it works beautifully with &lt;a href="https://tusky.app/"&gt;Tusky&lt;/a&gt; (on Android) and &lt;a href="https://tooot.app/"&gt;tooot&lt;/a&gt; (both on Android and iOS) - also consider &lt;a href="https://enafore.social/"&gt;Enafore&lt;/a&gt; as a PWA or web interface - and its integrated web interface is minimal but effective. No javascript, no cookies - clean web. More, the dev is responsive and open to patches and contributions. I've helped with some stress tests and contributed with instructions and patches to make it work on FreeBSD and NetBSD, and they've been merged immediately. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please, consider that&lt;/strong&gt;: It's not Mastodon. Some of the Mastodon API features are (currently) not supported so the experience could be different from the other solutions. Some Mastodon apps don't work (the official Mastodon app, for example, can't login). There's no open registration option (users should be manually registered from the cli) and account migration is not supported, at the moment. More, while it's not caching external media, locally published media will stay on the local drives (no S3 upload option), so be prepared to serve those files as well. Testing it from my FTTC connection, I almost DDoSed my internet connection for 15 minutes, publishing a photo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I’m suggesting another solution:&lt;/strong&gt; Actually, I'm suggesting to try snac2. I think it could be a great solution for a single user instance or for instances managed by tech people, as you can run it just 1 minute after the download. It's light, easy, straightfoward and the dev is a nice person. I'd suggest other solutions if the priority is to offer a full, feature rich Fediverse experience, with registration, big and featureful configuration panels, etc&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To sum up:&lt;/strong&gt; snac2 is a lightweight and effective way to join the Fediverse. Currently, it's my favourite solution for small communities as the "file only" approach and no dependencies are coherent with my ideas. I've migrated my instance from FreeBSD to NetBSD, from external datacenters to my home network and it's just been a matter of a single rsync &lt;strong&gt;&lt;em&gt;(-H)&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://gotosocial.org"&gt;GoToSocial&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;GoToSocial is a new microblogging platform. Its target it to be a light, customisable, easy to manage and integrated software. It is still at its early alpha stage, it should evolve into a beta at some point in ~~2023~~ 2024. It’s developed in Go and installation in easy and fast, it supports Postgres, Mysql and Sqlite. Basic functionalities have already been integrated and it can federate with (almost) all the other ActivityPub implementations, even if with some small problems. FreeBSD installation was easy and fast, as they provide a amd64 FreeBSD binary. Being written in go, it shouldn't be difficult to self-compile it for other architectures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I am suggesting it:&lt;/strong&gt; While it doesn’t have an integrated frontend (but Mastodon mobile apps or other frontends can be used, as &lt;a href="https://enafore.social"&gt;Enafore&lt;/a&gt; &lt;a href="https://github.com/BDX-town/Mangane"&gt;Mangane&lt;/a&gt;, etc.), it already supports many of the features you’d expect from an ActivityPub implementation. It's fast, suitable for installation on a low end hardware, can be deployed without a reverse proxy as has integrated support for Letsencrypt certificates. But…&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please, consider that&lt;/strong&gt;: It’s still at alpha stage. Things can still break and it doesn’t support any kind of account migration from Mastodon (while it’s supported by Pleroma and its forks).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I’m suggesting another solution:&lt;/strong&gt; While I think it could be a game changer, I think it’s bit early to deploy it unless you’re a very skilled and experienced administrator. You should understand its “quirks and features”, mainly tied to its alpha status. While I’ve installed and will keep installed a GoToSocial instance, at the moment I’ll just keep it in a test stage server in order to follow its development status. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To sum up:&lt;/strong&gt; GoToSocial could potentially become one of the most interesting ActivityPub microblogging platforms, at least for small and medium sized communities. It's a bit early to consider it ready for a production deployment, but it's already worth testing.&lt;/p&gt;
&lt;h3&gt;Other solutions&lt;/h3&gt;
&lt;p&gt;I’ve tried &lt;a href="https://takahe.social"&gt;Takahe&lt;/a&gt;, another microblogging platform. It’s under active and hard development, so, like GoToSocial, should be kept on the radar as it’s quite promising. I've just fired up a Linux docker installation to try it, so I haven't tried on FreeBSD.&lt;/p&gt;
&lt;p&gt;While I’ve also tried platforms like &lt;a href="https://pixelfed.org"&gt;Pixelfed&lt;/a&gt;, &lt;a href="https://joinpeertube.org"&gt;Peertube&lt;/a&gt;, and &lt;a href="https://funkwhale.audio"&gt;FunkWhale&lt;/a&gt;, which are all part of the Fediverse, they cater to specific needs and are distinct from the microblogging platforms that are the focus of this list.&lt;/p&gt;
&lt;h2&gt;Frontends&lt;/h2&gt;
&lt;p&gt;All the Fediverse backend implementations have their own specific features but the users will just interact via a frontend. While Mastodon - but also Pixelfed, Peertube, etc. - are presented as a specific stack (backend + frontend), there are many frontends that can interact via the Mastodon API.&lt;/p&gt;
&lt;p&gt;Of course, being the “Mastodon” API, not all the backends are perfectly compatibile/supported by all the frontends. Mobile apps use the Mastodon API, that’s why their compatibility with other implementations like snac2, Pleroma, Akkoma, etc. may not be perfect at all the times.&lt;/p&gt;
&lt;p&gt;I won’t describe them all. I’ll just enumerate the ones I’m using, with some notes:&lt;/p&gt;
&lt;h3&gt;Akkoma’s Pleroma-FE&lt;/h3&gt;
&lt;p&gt;With the 2022.12 release of Akkoma, Pleroma-FE has been evolved into a proper, nice looking PWA. After an initial configuration of its many options, I found it quite nice and effective. The only problem (common with many PWAs on iOS) is that when you suspend the app, it doesn’t detect it, so if you open it again after two hours, you’ll just see the recent posts, not all the posts of the last two hours. This is because of the way iOS deals with app suspend/resume and &lt;a href="https://github.com/elk-zone/elk/issues/750#issuecomment-1371966812"&gt;many PWAs don’t seem to understand they’ve been suspended.&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://github.com/BDX-town/Mangane"&gt;Mangane&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Mangane is a Soapbox fork that aims to improve compatibility with Akkoma. It is nice and clean and the developers are improving it to support Akkoma's features, so it's great. At the moment I've noticed  some visual problems, on iOS, with the icons -  &lt;strong&gt;but &lt;a href="https://github.com/BDX-town/Mangane"&gt;Guérin&lt;/a&gt; contacted me to ask for information (after reading this article) and opened an issue on to fix it&lt;/strong&gt;. Guérin has been nice and helpful, making Mangane even more appealing. I’m using it for planned posts on Akkoma, as Pleroma-FE doesn’t support them, yet. I'm also using Mangane as a daily driver, from time to time, as I like it.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://enafore.social"&gt;Enafore&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A light, fast, complete and usable web frontend. I’ve been using it when I needed a simple, clear frontend. It supports “hide replies”, which is great to improve timeline quality. It's among my favourite choices when using a webapp and is a good choice for snac2.&lt;/p&gt;
&lt;h3&gt;&lt;a href="https://elk.zone"&gt;Elk&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Elk is a frontend for Mastodon API. It’s nice, clear, eye candy, intelligent. The developers are nice people, open to suggestions. Development is a bit slower compared to the initial pace, but the app is already very complete and stable.&lt;/p&gt;
&lt;p&gt;Elk is definitely a very good piece of software, and I recommend to try it.&lt;/p&gt;
&lt;h2&gt;Moderation and final considerations&lt;/h2&gt;
&lt;p&gt;One of the things you should be considering is that deploying a piece of the Fediverse isn’t just installing a software and interacting with others. Actually, that’s just the easiest part of the experience, at least if you’re not creating a single user instance.&lt;/p&gt;
&lt;p&gt;Many of the people that joined the Fediverse in its early days decided to do it as they felt attacked on other social networks. In the last years, the commercial socials have proven to be the perfect place for negative people, attacking others without being blocked/stopped in an efficient way. &lt;em&gt;Hate causes addiction and the owners of those commercial socials make a lot of money if people interact, showing them ads every time they open the app/website&lt;/em&gt;. They make money (also) through people hating each other.&lt;/p&gt;
&lt;p&gt;The Fediverse gives the possibility to mute and block users, but also to mute and block entire instances. One of the main tasks of an instance’s administrator is to make sure that everything is ok. While you can define your own instance’s rules, other instances’ admins may block you if they find you’re federating by sending messages agains their rules.&lt;/p&gt;
&lt;p&gt;As an administrator, you’re also responsible of keeping your users’ data safe, to avoid sharing/providing illegal contents or offensive stuff. So you’re free to set your rules, but others are free to “defederate” you, if they don’t like the contents your instance is providing. While it’s not an issue for a single user instance, you must be quite careful when opening the registrations as you may find out you've been defederated because of (your) lack of moderation.&lt;/p&gt;
&lt;p&gt;There’s some strong criticism against the Fediverse because of this, as many see it as more “censored” than the traditional, commercial social networks. I don’t think it’s true, as you’re free to fire up your instance, decide your rules and act as you want. But you can’t impose others and their instances to follow you, even if you think you’re right. In a free world, everybody should be free to decide if they want to listen to you. But everybody should also be free to create a new space and start sharing their ideas. There’s not a central authority of contents, so only the single admins and users can choose what they want to see or avoid. There's not a commercially driven algorithm that may decide that you should see contents that will cause you anger and hate just because it generates traffic (and money) to the social's owner.&lt;/p&gt;
&lt;p&gt;The Fediverse can be a beautiful place to stay.&lt;/p&gt;
&lt;p&gt;&lt;mastodon-comments host="mastodon.bsd.cafe" user="stefano" tootId="111732122236947352"&gt;&lt;/mastodon-comments&gt;&lt;/p&gt;</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Stefano Marinelli</dc:creator><pubDate>Sun, 15 Jan 2023 09:29:45 +0000</pubDate><guid isPermaLink="false">https://it-notes.dragas.net/2023/01/15/deploying-a-piece-of-the-fediverse/</guid><category>fediverse</category><category>mastodon</category><category>freebsd</category><category>linux</category><category>docker</category><category>gotosocial</category><category>snac2</category><category>snac</category><category>mangane</category><category>twitter</category><category>facebook</category><category>social</category><category>hosting</category><category>server</category><category>web</category><category>akkoma</category><category>pleroma</category><category>elk</category><category>pixelfed</category><category>funkwhale</category><category>peertube</category><category>enafore</category></item></channel></rss>