From Self-Hosted FOSS to Proprietary PaaS: A Migration Story

First time to blog in three years!

As a person too busy with ${DAYJOB} the last few years, I wanted my remaining servers hosting groupware, file sharing, and news reading software to ideally manage themselves. 12 EUR/mo doesn’t mean you get a system administrator to do chores for you, instead you get to do the routines yourself. That could only continue for as much before we would get fed up and attempt the move back to being truly serverless™, which is what the post you’re reading is all about.

No servers for us would mean:

  • getting a smaller monthly bill thankfully to most personal cloud options costing less than a six EUR/mo VPS
  • doing nothing whenever another Debian / Red Hat / $DISTRONAME Security Advisory reports of a critical vulnerability
  • having nothing to maintain or update (which is a serious win, considering ${DAYJOB} involves enough system maintenance by itself)

Planning the move

Our self-hosted service inventory to migrate contains the following:

  • Gitea, a Git repository hosting in Go
  • Nextcloud, a groupware suite backend server that works as a private cloud, in PHP
  • Tiny Tiny RSS, an RSS feed reader and aggregator in PHP
  • Satellite, a Gemini server that only serves static content, in Go
  • (if you count this one as a service, that is) nginx, a web server hosting https://tdem.in, aka the website you’re reading this at, that also needs to serve a few occasional redirects, like music.tdem.in to the Last.fm page

The hardest challenge to solve with migrating off these three would be keeping exactly zero servers at hand. This would mean finding some other way to handle HTTP redirects from domains previously served at git.tdem.in, storage.tdem.in, and others, which a simple CNAME record doesn’t handle. Gladly for us, Cloudflare provides redirects as a feature of its web edge for free (if you’re fine with only having up to 10 of those), and the sole requirements to use them is to have Cloudflare handle DNS for your domain (which was already the case for tdem.in, and proved useful).

Finding a replacement to git.tdem.in was easy: GitHub was long the home to most of our public code. TTRSS would have to go in favor of something that runs locally, like NetNewsWire. Nextcloud has a pretty obvious alternative for Apple users.

As long as we were fine with stopping hosting the Gemini capsule (which was bound to happen at some point due to reducing interest and lack of energy to maintain gmnhg) and agreeing to the status quo of this website being our only home page, Satellite needed no replacement.

Therefore, our final listing of free or cheaper services to use for syncing consists of the following:

  • iCloud to handle files / calendar / contacts / tasks syncing / other groupware (only really works for Mac/iPhone users, but we’re rocking both anyway, so why not)
  • GitHub to host our ever-shrinking set of public projects
  • GitHub Pages to host this website
  • NetNewsWire to read RSS feeds locally and sync subscriptions with the built-in iCloud support
  • none for Gemini hosting, which was bound to be gone at some point anyway due to lack of energy to maintain gmnhg
  • Cloudflare to handle redirects

Cloudflare as the ultimate redirector

Performing redirects from a hostname to elsewhere is doable with Cloudflare in two simple steps:

  1. Creating a CNAME record pointing anywhere (we use @ as the target to prevent any unwanted side effects). The important bit is that the “Proxied by Cloudflare” toggle needs to be ON, so the traffic reaches the Cloudflare edge.

CNAME records at Cloudflare CNAME records at Cloudflare

What actually ends up on public DNS looks like this:

CNAME records, transformed by Cloudflare into A records onto its edge CNAME records, transformed by Cloudflare into A records onto its edge

  1. Creating a dynamic redirection rule that responds with 302 whenever traffic matching your hostname reaches Cloudflare:

Cloudflare dynamic redirection rule matcher Cloudflare dynamic redirection rule matcher

Cloudflare rule target, responding with a 302 Cloudflare rule target, responding with a 302

The end result is that our traffic ends up where destined:

Cloudflare redirects git.tdem.in to GitHub, as advertised Cloudflare redirects git.tdem.in to GitHub, as advertised

Gitea

Relatively easy to migrate, as long as most of the repos are private, feature no CI/CD, no content to migrate besides code, etc. The list of repos is trivially obtainable through <profile> -> Admin Panel -> Content -> Repositories.

This small snippet automates most of the routine, except creating the repository at GitHub:

git_mirror () {
    git clone --mirror "https://git.tdem.in/${1}.git" $1
    cd $1
    git remote add github "https://github.com/${1}.git"
    git push -u github --all
    git push github --tags
    cd -
}

Invoking it, as repositories at GitHub get created and reconfigured to be private and contain no wiki / Actions / etc, is as easy as:

git_mirror tdemin/repository1
git_mirror tdemin/repository2

Memo: Gitea uses the same wiki Markdown hosting scheme as GitHub, with ${REPOSITORY_NAME}.wiki.git being the URL for the Git repository containing all of your pages. Migrating that would simply mean an extra git_mirror.

The last bit remaining would redirecting everyone1 hitting git.tdem.in from history or links on the public web, which is configurable with Cloudflare as shown above.

Nextcloud

The amount of time spent on migrating from a groupware software piece to another directly depends on the volumes of information stored inside the former. The scenario for storage.tdem.in included the following, for one user:

  • calendar events
  • reminders (technically fancy calendar events)
  • contacts
  • files

Files are taken care of by copying them to the iCloud drive folder. To make syncing ~/Desktop and ~/Documents seamless like Nextcloud offers OOB2 you might want to enable the Desktop & Documents folders in iCloud sync settings. As an added bonus, you get file URL sharing directly from Finder.

That is, until it works That is, until it works

Calendar events are trivially moved to the relevant calendar in two clicks per event. While sounding like a tedious job, that’s doable in a few minutes even for the largest agendas thanks to the built-in snappy macOS app. Reminders get moved into iCloud-hosted lists with the help of mass drag-and-drop (unless you care about lots of historically completed entries. I do not).

As easy as it gets with the right app for the job As easy as it gets with the right app for the job

There’s an option to extract your calendar entries through CalDAV and then import it using https://caldav.icloud.com, but I didn’t care enough to do that. The events in my calendar were trivial enough to click through to move to iCloud-provided calendars. The Apple’s calendar app is friction-free enough.

Contacts proved to be a simple task for a mass vCard export/import. A few records needed fixing after the import, but that’s a five to ten minute-job even for a contact book of hundreds of entries.

Tiny Tiny RSS

There’s no real data export option other than using OPML, but that one just gets imported into your client of choice, and you’re done. I recommend NetNewsWire as a brilliant macOS / iOS implementation that can also sync feeds across devices with iCloud, but the choice is ultimately up to you.

You might still want to keep the dump of your TTRSS database for archival purposes.

That&rsquo;s&hellip; quite a number of entries to get rid of. :( That’s… quite a number of entries to get rid of. :(

GitHub Pages

Migrating an existing Hugo blog with a well-defined CI setup mostly concludes to following the instructions for setting up GitHub Pages and then doing the routine to get it work with a custom domain. My sole nitpick was that GitHub Pages can be served from any domain, not just <username>.github.io, which helped with keeping the repository in its canonical place.

In the end, the whole chore concluded to the following GitHub Actions workflow diff:

-      - name: Build Gemini site with gmnhg
-        uses: docker://ghcr.io/tdemin/gmnhg:v0.4.2
+      - name: Deploy to GitHub Pages branch
+        uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847
         with:
-          args: gmnhg -output ./gmnhg
-      - name: Deploy web site
-        # v6.0.0, strict commit pinning due to handling secrets
-        uses: Burnett01/rsync-deployments@45d84ad5f6c174f3e0ffc50e9060a9666d09c16e
-        with:
-          switches: -avzr --delete
-          path: hugo/
-          remote_path: ${{ secrets.DEPLOY_ROOT_PATH }}/blog.tdem.in
-          remote_host: ${{ secrets.DEPLOY_HOST }}
-          remote_user: ${{ secrets.DEPLOY_USER }}
-          remote_key: ${{ secrets.DEPLOY_KEY }}
-      - name: Deploy Gemini site
-        # GitHub Actions doesn't allow anchors in workflow YAML, so
-        # duplicating this is required
-        uses: Burnett01/rsync-deployments@45d84ad5f6c174f3e0ffc50e9060a9666d09c16e
-        with:
-          switches: -avzr --delete
-          path: gmnhg/
-          remote_path: ${{ secrets.DEPLOY_ROOT_PATH }}/gemini
-          remote_host: ${{ secrets.DEPLOY_HOST }}
-          remote_user: ${{ secrets.DEPLOY_USER }}
-          remote_key: ${{ secrets.DEPLOY_KEY }}
+          github_token: ${{ secrets.GITHUB_TOKEN }}
+          publish_dir: ./hugo
+          cname: tdem.in

Setting up redirects for both www.tdem.in and blog.tdem.in as outlined in the Cloudflare chapter above helps for the remainders of blog.tdem.in on search engine databases.

As tdemin.github.io is now served from this repository, this required importing content we might have had in there. Gladly, this only meant copying a single blog post for the archive.

Conclusions

Whether or not we won or lost more by migrating depends on the priorities. Pros are as follows:

  • less costly monthly bill (60% of the original costs after one adds the VPN server costs, that of two 6 EUR/mo VPS instances)
  • virtually no maintenance required
  • better groupware OS integration for Apple devices, where syncing reminders whenever they change/get added just works™ and doesn’t get stuck from time to time which is what occasionally happened on both latest iOS 17 and macOS 14 with Nextcloud CalDAV entries installed with a configuration profile

What we lost:

  • the ability to have a background always-on RSS updater that would ensure we lose no entries in-between connections (which is important for high-volume collective blogs and news outlets)
  • freedom in picking groupware client software, which never really existed on Apple hardware anyway

I stand by the option that was a net positive.


  1. This means pages like links to commits, issues, etc, are likely to break, since GitHub might not necessarily do the URI scheme Gitea’s web UI does. This also includes Google Search, as Gitea’s workarounds to get non-canonical pages (random commit pages in random languages, etc) out of search engine indexes are imperfect to this day. YMMV. ↩︎

  2. Out-of-the-box ↩︎

Tags: