Self-Hosting Your Git Forge: What Moving to Forgejo Means for Your .NET Pipelines
A trending case for leaving GitHub for self-hosted Forgejo has reignited the sovereignty debate. Here's what the migration actually costs once your CI/CD runs .NET.
Jean-Pierre Broeders
Freelance .NET Developer
Self-Hosting Your Git Forge: What Forgejo Means for Your .NET Pipelines
A blog post titled "Why I'm leaving GitHub for Forgejo" hit the Hacker News front page this week and racked up hundreds of comments. The author, Jorijn Schrijvershof, moved his canonical Git host to a self-hosted Forgejo instance running on a single NUC, and framed the decision around ownership rather than uptime. The timing is loaded: the same week, a separate story about a malicious VS Code extension breaching thousands of GitHub repositories was also trending. The two threads rhyme.
I'm not here to tell you to rip GitHub out tomorrow. I'm a .NET freelancer; most of my clients live on GitHub or Azure DevOps and will stay there. But the migration argument is worth taking seriously precisely because it's no longer fringe — and because the part everyone underestimates is what happens to your CI/CD once the forge is yours. So this is the practitioner version: what's actually driving the move, and what it costs when your pipelines build and test C# code.
The argument, briefly
Strip away the heat and the case for leaving rests on three structural facts, not on outages.
First, GitHub no longer has its own CEO. After Thomas Dohmke stepped down in August 2025, GitHub was absorbed into Microsoft's CoreAI division. The brand survives; the independent leadership doesn't. The old "Microsoft keeps it at arm's length" argument stopped being true.
Second, the training-data default flipped. As of April 24, 2026, interaction data from Copilot Free, Pro, and Pro+ is used to train models unless you opt out — and there's no repository-level switch. As a maintainer you cannot say "don't train on interactions inside my repo"; each contributor has to opt out individually.
Third, jurisdiction doesn't move when data does. GitHub Inc. and Microsoft Corp. are US companies, so their data sits in scope of FISA Section 702 and the CLOUD Act regardless of where it's physically stored. EU data residency solves location, not jurisdiction.
The reason this isn't just one developer's opinion: on April 27, 2026 the Dutch Ministry of the Interior soft-launched code.overheid.nl, a self-hosted Forgejo instance, picking Forgejo over GitLab specifically because it's fully open source with no open-core split. When a national government with serious lawyers reaches the same conclusion the week before a solo consultant does, the decision has left the fringe.
Why Forgejo and not GitLab
Two things tip it. Licensing: GitLab is open core — the Community Edition is free, but a lot of what you'd want in production lives behind the Enterprise license. Forgejo went the other way, relicensing to GPLv3+ in v9.0 (August 2024) to stay copyleft and resist commercial capture. Governance: Forgejo lives under Codeberg e.V., a member-governed non-profit registered in Berlin. The project forked from Gitea in December 2022 precisely because a company took control of the trademarks; the license change is the lesson learned. Forgejo v15.0 LTS shipped on April 16, 2026 with support through mid-2027.
For a .NET shop the practical headline is that Forgejo ships Forgejo Actions, which aims for familiarity with GitHub Actions. "Familiarity," not "compatibility" — and that distinction is the whole story for your pipelines.
What actually happens to a .NET pipeline
Here's a normal GitHub Actions workflow for a .NET service: restore, build, test, publish. On Forgejo Actions, most of it just runs. The YAML below works on a self-hosted Forgejo runner almost unchanged:
name: build-test
on:
push:
branches: [main]
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v5 # pin to v5, see below
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release --logger trx
The NuGet caching strategy is identical to GitHub's — the actions/cache API is reimplemented on the Forgejo side. But there are sharp edges you only find by hitting them:
permissions:blocks at workflow level are silently ignored. If your security model leans on scoping theGITHUB_TOKENper job, that assumption evaporates. You have to enforce least-privilege at the runner and token-registration level instead.actions/checkout@v6broke authenticated checkout on non-GitHub runners in early 2026. The fix is unglamorous: pin everything to@v5.actions/upload-artifact@v4needs the Forgejo-hosted fork, not the upstream one. Artifact-heavy pipelines need an audit.- OIDC works, but the key is different. GitHub uses
permissions: id-token: write; Forgejo usesenable-openid-connect: true. If you federate into Azure to deploy without long-lived secrets, this is the line you'll rewrite.
That last point matters most for .NET teams, because the clean pattern for deploying to Azure from CI is OIDC federation — no AZURE_CREDENTIALS secret sitting in the repo. It still works on Forgejo, but the workflow key and the trust configuration on the Azure side both need adjusting. Budget an afternoon, not five minutes.
And Dependabot doesn't exist on Forgejo. The standard replacement is Renovate, self-hosted on the same runner. It does the same job with more configuration — which for a NuGet-heavy solution is arguably an upgrade, since you get fine-grained control over grouping and automerge rules.
The runner is the real project
If you self-host a forge, the forge itself is the easy part. Forgejo plus Postgres plus a reverse proxy is a boring, solved problem. The hard part — and the part that should keep you up at night — is the CI runner, because that's the thing that executes untrusted code.
Think about what your runner actually does. It runs dotnet restore, npm install, composer install against lockfiles, on a schedule, often triggered by a dependency bot. That means it executes third-party lifecycle scripts. The trending VS Code extension breach this week is the same shape of attack: malicious code riding the developer supply chain. A self-hosted runner that auto-merges Renovate PRs within the hour is exactly the soft target those attacks look for.
So the runner's job isn't to run code. It's to contain code while it runs. The setup described in the source article layers five defenses, and the principle generalizes well beyond one homelab:
- A dedicated KVM virtual machine for the runner, so the host kernel isn't shared with job containers. A kernel CVE inside a job has to escape the VM before it touches the host.
- gVisor (
runsc) as the default container runtime inside that VM, intercepting syscalls in user space. An escape now has to break gVisor and KVM. - A weekly destructive rebuild — the entire runner VM is destroyed and recreated from a fresh base image, so no persistent state survives more than seven days, and each rebuild picks up that week's patches.
- An nftables egress filter that lets the runner reach package registries and the forge, but blocks the local network ranges entirely. A compromised job can't pivot to your LAN or the router admin.
- Scope-bound runner tokens, never admin-scoped, so a leaked token can't register runners outside its scope.
None of these primitives are novel. What's worth copying is the mindset: assume any single layer can fail and design so the next absorbs it. If you've ever read my take on secret rotation or webhook HMAC verification, this is the same defense-in-depth reasoning applied to the build host instead of the request path. The runner is where self-hosting stops being a weekend project and becomes real engineering.
When this is worth it — and when it isn't
I'll be honest about the trade-offs, because the migration costs are real.
It's worth considering if digital sovereignty is a genuine requirement — you handle data where US jurisdictional exposure is a contractual or regulatory problem, or you simply want to own the platform your code runs on. It's also a fit if you have the appetite to run infrastructure and want CI without per-minute billing surprises.
It's not worth it if your team has zero capacity for ops, if you're heavily invested in GitHub-specific features (Codespaces, the Apps marketplace, Advanced Security), or if your contributor base is the GitHub social graph and discoverability beats ownership. A managed Forgejo host closes some of the ops gap, but you still own the migration.
The pragmatic middle path — and the one the Dutch government took — is not a wholesale rip-and-replace. Make Forgejo canonical for the work that needs sovereignty, keep GitHub as a mirror for discovery, and revisit later. For most of my clients that's the realistic answer: a hybrid where the forge you control holds what matters, and the public mirror keeps the contributor path intact.
The takeaway for .NET teams
The sovereignty argument is no longer hypothetical, and Forgejo is now mature enough to be a credible canonical forge. But evaluate it with your eyes open on the part that actually bites: your pipelines. Forgejo Actions gives you familiarity, not compatibility — pin your actions, rewrite your OIDC federation, swap Dependabot for Renovate, and treat the runner as a security project in its own right. Do that, and self-hosting is achievable on hardware that fits on a desk. Skip the runner hardening, and you've just built a beautifully sovereign way to get owned.
Source: "Why I'm leaving GitHub for Forgejo" by Jorijn Schrijvershof — discussion on Hacker News. The .NET pipeline analysis and recommendations are my own.
