I’ve been on a campaign for a few months to redefine how software is hosted on the internet. It’s been a decade since I worked in the enterprise and nothing much has changed.
It’s beyond time for that change to happen.
For a first example I chose to tackle my git hosting.
GitHub, GitLab, and Friends
I absolutely love GitHub. It was how I finally learned how to use a distributed version control system (dVCS) over the centralized systems like Subversion and Team Foundation that I’d learned earlier in my career. The only thing I don’t love about it is the fact that it’s a centrally-hosted tool. I can’t event run it myself without an expensive enterprise license

I’ve used GitLab in a few organizations and it’s a powerful alternative. You can host it yourself as an enterprise for a cost, and it has somewhat more powerful tools than GitHub alone. Self-hosting is also both permitted and fairly common … but not for the feint of heart due to the complex configuration. Especially when you get into CI/CD integration.

Finally, I played a bit with Gitea. It’s (mostly?) open source and remarkably easy to configure. I don’t have any problems with the software whatsoever and ran it personally for a few months myself. Recently, though, I switched over to Forgejo due to some more comprehensive documentation on integrating my git server with Tailscale.1There’s no functional difference between Forgejo and Gitea that I can see as a user. The former is a fork of the latter and, under the hood, they still run a lot of the same code. That said, they’ve been gradually diverging over time so your mileage may vary …
Taking git Private
I’ve used Tailscale for a while now to network all of my devices together. My primary desktop, my laptops, my phone, the NUCs running my personal cloud, my mail server. Every device in my stack is configured to connect into my personal Tailnet, giving me the ability to leverage exit nodes for IP routing and services hosted for my eyes only.
My personal NextCloud server runs on a machine on my desk, backs up to a private cloud instance, and lets me sync and back up my machines and mobile device regularly. I wanted to do the same with Forgejo, and it was much easier than I expected, thanks to a Forejo-hosted example Docker configuration.
The project README explains how things can be built by default. My installation consists of:
- A Forgejo instance connected directly to my Tailnet
- A pre-configured GitHub-style action runner for CI/CD
- Mail delivery via AWS SES
My installation differs from the example mostly by bumping to the latest versions of the forgejo/forgejo
and forgejo/runner
images but otherwise follows the stock example to the letter.

Private CI/CD and Packages
The private hosting of code is only part of the journey. While all of my devices can talk to the code repository, a stronger story would be integrating proper CI/CD pipelines and allowing for built artifacts to be conveyed over my Tailnet as well. This was, unfortunately, too difficult a journey with my older Gitea setup due to the way I’d configured the host.
Forgejo provided the opportunity to begin again!
I started with the simplest of Dockerfile
s defining a basic “hello world” container, aiming to package a container image and deliver it to a remote machine directly.
FROM alpine:latest
CMD ["echo", "hello world!"]
The hardest part wasn’t the build itself, it was appropriately tagging and pushing the subsequent images into my private repository. This was difficult due to the fact that the repository itself is provided by Forgejo and only on my Tailnet – so any ephemeral container run by the CI/CD pipeline needs to also be present on the Tailnet in order to talk to the repository!
After some trial and error I was able to identify a fork of the stock Tailscale build action, along with the appropriate Docker Buildx option to properly set DNS within the build container. This allowed for my hello world application to build, tag, push …
name: Build Images
run-name: ${{ gitea.actor }} is running ci pipeline
on: [ push ]
jobs:
build:
runs-on: ubuntu-22.04
if: gitea.ref == 'refs/heads/main' || gitea.ref == 'refs/heads/actions'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Set outputs
id: vars
run: |
echo "commit=$(git log -1 --format='%H')" >> $GITHUB_OUTPUT
echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT
echo "gitref=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Connect to Tailnet
uses: https://github.com/Pikachews/tailscale-action@v1
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- name: Login to Docker Registry
uses: https://github.com/docker/login-action@v3
with:
registry: git.[my-tailnet].ts.net
username: ericmann
password: ${{ secrets.DOCKER_TOKEN }}
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v6
with:
context: .
push: true
tags: |
git.[my-tailnet].ts.net/ericmann/ci-test:latest
git.[my-tailnet].ts.net/ericmann/ci-test:${{ steps.vars.outputs.branch }}
git.[my-tailnet].ts.net/ericmann/ci-test:${{ steps.vars.outputs.gitref }}
… and eventually pull the generated Docker container artifacts!
$ docker run --rm git.[my-tailnet].ts.net/ericmann/ci-test:latest
Unable to find image 'git.[my-tailnet].ts.net/ericmann/ci-test:latest' locally
latest: Pulling from ericmann/ci-test
f18232174bc9: Already exists
Digest: sha256:170cf57f399eaf7d8ed4ad1590eddaab5ae24ce608bae48d55ead959cc51468b
Status: Downloaded newer image for git.[my-tailnet].ts.net/ericmann/ci-test:latest
hello world!
OK, Geek. What’s Next?
Me hosting a private git repository doesn’t do you any good. The fact that I had this entire stack up and running in less than 20 minutes does.
Over the next several months I’m going to be packaging my configurations into a single tool you can use to automate your own infrastructure. Do you want your own private git server? Your own container registry? Your own blog? Your own Mastodon instance? A private Matrix server for friends and family?
My plan is to provide all of this and more. A complete, DIY infrastructure kit for solo developers and small businesses who lack formal SRE capabilities. I’ll be building in the open, so you can keep track of progress:
- On this blog
- On the new site for Displace Technologies (the name under which I’ll build this)
- On social media through Twitter/X and Mastodon
Follow and stay tuned for more!
- 1There’s no functional difference between Forgejo and Gitea that I can see as a user. The former is a fork of the latter and, under the hood, they still run a lot of the same code. That said, they’ve been gradually diverging over time so your mileage may vary …