Blog / / 5 min read
Running my own Git server
Why my code lives on a private Gitea server at home, how the Actions runners are set up, and how it gets backed up.
Most of my code lives on a Gitea server running on my NAS. It’s local only: you can only reach it from my homelab network, or over VPN if you’re one of the people I’ve given access to. It’s not on the public internet at all. GitHub is still where I go for open source and anything public facing, but my homelab, my notes and my configs live at home.
So why bother? Honestly, I wanted to understand what a Git platform actually is when you take away the nice website. And it turns out running one teaches you a lot about the stuff I do at work anyway: reverse proxies, databases, backups, CI runners, secrets.
The shape of it
Gitea runs on Magnus, my NAS, as a few containers: Gitea itself, a database next to it, and a runner. It sits behind my reverse proxy for HTTPS, and Git over SSH works too, but only from inside the homelab network or over VPN. There’s no public route to it, which means a lot less to worry about. Nobody is going to brute force a login page they can’t reach.
Before I set it up I made myself a checklist in Obsidian: email for password resets, backups that land somewhere off site, everything configured in the compose file, and SSH working for normal Git operations. That list ended up being pretty much the whole project.
Runners
Gitea Actions is close enough to GitHub Actions that most workflows just work. I have runners in three places now:
- On the NAS, next to Gitea.
- On freyja, my Docker host.
- On astrid, my NixOS box, which runs jobs right on the machine so they can use Nix for deploys. I wrote about that in Why my servers run NixOS.
The thing I like most about this is that a runner only ever reaches out. It asks Gitea “got any jobs?”, runs them, and reports back. Nothing has to connect into it, so adding the runner on freyja didn’t mean opening anything up on its firewall.
When a job comes in, Gitea hands it to whichever runner is free and has the right labels. If I ever need a job to run on one specific box, I just give that runner its own label.
Backups
This is the part that matters. If Gitea dies and I lose everything, that’s my notes, my configs, my homelab, all of it.
Every night the whole Gitea stack gets backed up to Backblaze B2. It’s encrypted on my side before it leaves the house, so B2 only ever sees scrambled chunks. The backup job itself is deployed by CI, which means the thing protecting my Git server is defined in my Git server. That’s a little funny, but it means I’m never running a version of the backup script I forgot to commit.
I’ve also got Renovate opening PRs when images update, and a workflow that checks every compose file on push. More on that in One Compose project per directory.
Things that went wrong
Drift. For a while the compose file actually deployed on the NAS and the one in my repo weren’t the same. I wrote a note in the README admitting the deployed copy was the real one and the repo was stale. It’s the most homelab thing ever. You fix something live, it works, and the change never makes it back to the repo. That’s a big part of why I keep pushing more of the lab toward “deploy from the repo or don’t deploy at all.”
Secrets in repos. Even on a private server, it’s way too easy to commit something you shouldn’t, and private doesn’t stay private forever. So now every new project gets a .env.example with fake values, and the real .env is gitignored from the first commit. If something sensitive ever lands in history, the answer is rotate first and clean up second, because deleting the file doesn’t take it out of history.
Would I recommend it?
If you just want a place to put code, use GitHub. It’s free and it’s great.
If you want to learn how all the pieces of a platform fit together, self hosting Gitea is one of the best homelab projects I’ve done, and keeping it behind your VPN makes it a lot less scary to run. It’s small enough to run on anything, and it touches networking, storage, databases, CI and backups all at once.