A Self-Hosted Publishing Workflow for This Website

Nextcloud, Gitea, Hugo, and a Raspberry Pi — all on machines I control


I wanted to host my own website without giving up the convenience of modern cloud tools.

The usual easy version would be to spread the pieces across familiar services: Google Drive or OneDrive for files, GitHub.com for version history, and a hosted platform for the website itself. That can work. GitHub is a very useful hosted Git service, but it is still a service owned and operated by someone else.

I wanted the self-hosted version: the same basic comfort, but with the important parts running on machines I control.

That meant I needed a few things. A home server, which I already had with my Umbrel. A working folder that syncs between my desktop, laptop, and maybe even my phone. And a small publishing machine that can turn the ready version into the live website.

For that publishing machine, I use a Raspberry Pi. A Raspberry Pi is a small single-board computer. It is not powerful like a desktop PC, but it is more than enough for small server jobs. It can sit quietly at home and do one job reliably.

The important design choice is the publishing step. Saving a file should not be the same thing as putting it on the live website. Writing and publishing feel related, but they are not the same job.

My first version blurred that line. The Pi checked the Nextcloud folder for updates and immediately served the current files online. It worked, but it treated every synced change as a publish. Since writing often means drafts, experiments, and unfinished edits, that was a problem. A half-written paragraph, a broken experiment, or an unfinished layout change could become the live site.

Gitea became the solution. It gave me a place to record only the versions I had decided to publish.

The shape of the system is:

Laptop or desktop
  writes files in a Nextcloud folder

Umbrel server
  runs Nextcloud for sync
  runs Gitea for publishing history

Publishing Pi
  pulls the ready files
  commits the publishable version
  builds and serves the live website

The rule is:

syncing is automatic
publishing is a decision

Writing

The folder I write in lives in Nextcloud:

~/Nextcloud/Writing/Website

Nextcloud is a self-hosted file sync system running on my Umbrel. It fills the same role that Dropbox, Google Drive, or OneDrive often fill: keep a folder available on multiple devices. The difference is that this one runs on my own server.

The website files are mostly plain text. That makes them easy to sync and easy to edit. Nextcloud does not need to understand the website. It only needs to keep the folder available everywhere.

That is useful while writing. Drafts and experiments can move between devices without becoming public. Sync only answers the question:

Where should this file be available?

Publishing needs a different question:

Is this version ready?

That is where Git comes in.

Publishing

Git is the version history. It turns a group of file changes into one named version that I can inspect later or roll back to.

Gitea is the place where that Git history lives. It is like a small self-hosted GitHub, running on my Umbrel instead of somewhere outside my own setup. In this workflow, Gitea is not where I write. It is where the publishable versions get recorded.

The important choice is that the publishing Git working copy only lives on the Pi. My laptop does not commit or publish directly. It only tells the Pi when to publish.

From my writing folder, publishing a normal post looks like this:

./publish-posts.sh "Add post about the publishing setup"

That command sends a message to the Pi over Tailscale. Tailscale gives my devices a private network, so the laptop can reach the Pi without exposing it to the public internet.

The Pi refreshes its copy from Nextcloud, commits the post, pushes that version to Gitea, and rebuilds the live site.

There is also a full publish command:

./publish-full.sh "Update the site layout"

That one publishes the whole website source. I use it when I want broader site changes to go live, not just a post.

I like having both commands because most of the time I am publishing writing. If I am still tinkering with the site itself, I can keep using the posts command and leave those wider changes out for now.

After the Git part is done, Hugo turns the website source into static files. Static files are just the finished pages the browser can load. A normal web server called nginx serves those files.

Hugo also generates an RSS feed automatically. That means this website has a feed at https://blog.tjdigital.co/index.xml without any extra configuration. If you want to follow new posts in an RSS reader, you can add that link directly. I cover RSS readers in more detail in Build a Reading Feed You Control.

The Raspberry Pi, Hugo, Gitea, Tailscale, and Nextcloud pieces can each become their own notes later as the site grows.

Convenience While Staying Self-Hosted

The part that matters most is not the list of tools. It is that the workflow stays convenient while it stays self-hosted.

I can save files whenever I want, on whichever device I am using, without treating every save as a public update. When a post is ready, I can publish it with one command. Nextcloud, Git, Gitea, Hugo, Tailscale, and the Pi each do their own small part behind that.

That does add some moving parts. This is not simpler than using one hosted platform for everything. But day to day, it is about as frictionless as I could make it while keeping the workflow under my own control.

That is what I like about it. I had a specific problem, and I could combine separate pieces into something that works the way I need it to work.

Have a thought about this post? Reply by email at tj.unfrozen851@passinbox.com.