Leaving GitHub? What the Codeberg and self-hosting exodus means for your .NET workflow

112 hours of downtime in a year, an ICE contract, and mandatory AI. A practitioner's look at the GitHub exodus, with Forgejo self-hosting via Docker and a working .NET CI pipeline.

Jean-Pierre Broeders

Freelance .NET Developer

July 10, 20269 min. read
Leaving GitHub? What the Codeberg and self-hosting exodus means for your .NET workflow

A How-To Geek piece climbed high on Hacker News this week: "Why developers are ditching GitHub for Codeberg and self-hosting alternatives" (discussion here, 349 points and 240-plus comments). It hit a nerve. Ghostty left in April, Zig went back in November 2025, and the HN thread is a genuine fight about whether any of this matters.

I'm a freelance .NET developer. A good chunk of my income runs through repos hosted on GitHub, CI that runs on GitHub Actions, and NuGet feeds wired into all of it. So when I read this article, my first reaction wasn't "finally, I'm out." It was: how bad is it really, and what does it cost me to do something about it. Those are the two questions I want to answer honestly, with code you can run tonight.

where the complaints come from

Let's start with the facts from the article, because they're more concrete than the griping on X suggests. Between May 2025 and May 2026, GitHub racked up around 112 hours of downtime across 48 major outages. GitHub Actions was behind 57 of those incidents. That's not noise. When your pipeline stalls in the middle of a release day because someone else's runners fell over, you feel it in your invoicing.

Then there's the 200,000 dollar contract between GitHub and ICE, which angered creators and staff alike. And the AI direction. CEO Thomas Dohmke said, in as many words, that developers should embrace AI "or get out of this career." Mitchell Hashimoto, the person behind Ghostty, put his frustration like this: "It's not a fun place for me to be anymore." Copilot pushed into every corner of the UI doesn't land well with everyone.

You don't have to agree with every objection to understand why people are running for the exit. For me, the downtime carries the most weight. The rest is principle, and principles are personal.

the candidates, quickly

The article lists a handful of alternatives. Codeberg is a German nonprofit running on Forgejo, with privacy and ethics as its founding idea. Forgejo itself is a fork of Gitea, and Gitea is a lightweight Git server written in Go that runs on almost nothing. GitLab most people know already, with a full DevOps suite and a self-hosting option. And Sourcehut, for the folks who want their workflow in email and patches.

For a .NET freelancer, the practical choice comes down to two flavours. Option one: put your public code on Codeberg and leave GitHub alone. Option two: host your own Forgejo instance and keep control in-house. I'm going to walk through the second one, because that's where the real lesson lives.

self-hosting Forgejo with Docker Compose

This is the part people are scared of and shouldn't be. A working Forgejo instance is up in ten minutes. Here's a compose.yml I run on a small VPS myself:

services:
  forgejo:
    image: codeberg.org/forgejo/forgejo:11
    container_name: forgejo
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=${DB_PASSWORD}
      - FORGEJO__server__DOMAIN=git.yourdomain.com
      - FORGEJO__server__ROOT_URL=https://git.yourdomain.com/
    volumes:
      - ./forgejo-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    container_name: forgejo-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=forgejo
    volumes:
      - ./postgres-data:/var/lib/postgresql/data

Put DB_PASSWORD in a .env file next to the compose file and start with docker compose up -d. The web installer opens on port 3000. One detail that bites people: the SSH port maps to 222 on the host, so your Git remote becomes ssh://git@git.yourdomain.com:222/.... Miss that and you'll spend an hour debugging why git push refuses to talk. Don't ask how I know.

Put a TLS-terminating reverse proxy in front, Caddy or Traefik, and you get HTTPS with no fuss. All the state lives in two folders on your host, so a backup is literally a tar of forgejo-data plus a database dump. No magic.

but what about CI? Forgejo Actions

Here's the surprise most people don't see coming. Forgejo ships its own Actions implementation that largely mirrors GitHub Actions syntax. Your YAML moves over with minimal edits. The runner is a standalone binary you register against your instance.

First, register the runner. Grab a token from the admin settings or your repo settings, then:

docker run --rm -it \
  -v $(pwd)/runner-data:/data \
  code.forgejo.org/forgejo/runner:6 \
  forgejo-runner register --no-interactive \
    --instance https://git.yourdomain.com \
    --token YOUR_RUNNER_TOKEN \
    --name dotnet-runner \
    --labels docker:docker://mcr.microsoft.com/dotnet/sdk:9.0

That last label is the interesting bit. I point the label straight at the official .NET SDK image so my jobs run in a container with the full toolchain already inside. No setup-dotnet step needed, the SDK is baked in.

Now the workflow. This file goes in your repo under .forgejo/workflows/ci.yml:

name: build-and-test
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: docker
    container:
      image: mcr.microsoft.com/dotnet/sdk:9.0
    steps:
      - uses: actions/checkout@v4

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --no-restore -c Release

      - name: Test
        run: dotnet test --no-build -c Release --logger "trx;LogFileName=results.trx"

      - name: Publish artifact
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: "**/results.trx"

Lay this next to your existing GitHub Actions workflow and you'll notice it's almost identical. actions/checkout and upload-artifact are mirrored from a compatible action index, so they just run. The biggest differences: you use runs-on: docker instead of ubuntu-latest, and not every exotic marketplace action exists in the Forgejo world. For a standard .NET pipeline of restore, build, test, and publish, you rarely hit that wall.

my actual advice: don't migrate, mirror

Here's where I part ways with the loudest voices on HN. Walking away from GitHub completely is a bad move for most freelancers. Not out of loyalty, but because your clients, your open-source dependencies, and your network all live there. A pull request from a stranger almost never lands on your self-hosted Forgejo.

What's smart is making Forgejo your source of truth and mirroring to GitHub. You keep control over your code and your CI while staying visible on the platform where the world is looking. Forgejo has push mirroring built in, but you can also wire it explicitly in Git:

# Forgejo is your origin, GitHub is an extra push target
git remote add origin ssh://git@git.yourdomain.com:222/jp/my-project.git
git remote set-url --add --push origin ssh://git@git.yourdomain.com:222/jp/my-project.git
git remote set-url --add --push origin git@github.com:jp/my-project.git

# One push, two destinations
git push origin main

Now every git push writes to both. If GitHub Actions goes down for a day during a release, your Forgejo runner keeps chugging and you still ship on time. That's exactly the scenario that made those 112 hours of downtime hurt, and you caught it with three lines of config.

Want it the other way around, with GitHub as the source and Forgejo as a warm backup? Set up a "pull mirror" in Forgejo that periodically fetches from GitHub. I run the first variant myself, because I don't want my CI depending on a status page I have no influence over.

moving an existing repo without losing your history

Adding a remote is one thing. Bringing years of issues, pull requests, and labels with you is another, and that's where a real migration gets sticky. Forgejo has a migration tool that pulls your repo plus its metadata straight through the GitHub API. In the web UI you pick "New Migration", paste the GitHub URL and a personal access token, and tick what you want to bring over: issues, PRs, releases, wiki, milestones. It works better than you'd expect. Your PR history comes across as issues with the original discussion underneath.

The thing to watch as a .NET dev: your NuGet packages. If you publish to GitHub Packages, that feed doesn't move on its own. Forgejo has its own package registry that speaks NuGet, so you can repoint your nuget.config at your own instance:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="forgejo" value="https://git.yourdomain.com/api/packages/jp/nuget/index.json" />
  </packageSources>
</configuration>

A dotnet nuget push with an API key from your Forgejo profile drops the package right in. For internal libraries that only you consume, that's fine. Public packages I leave on nuget.org, because that's where everyone actually searches.

the trade-offs nobody says out loud

Self-hosting is not a free lunch. You now run a database yourself, you have to test your backups (a backup you've never restored doesn't exist), and you own the security updates for your Forgejo image. Budget half an hour of maintenance a month. For me that's worth it. For a team of twenty with its own platform group, the math looks different.

And Codeberg? Fine for open source, but it's a nonprofit run by volunteers with limited capacity. They've said several times lately that growth is coming in fast. Don't just dump your commercial production CI on it without thinking about what you give back, whether that's a donation or your own runner.

The real win isn't which logo sits in your browser. It's realising Git was designed to be decentralised and that you can have more than one remote. One platform as a single point of failure is a choice, not a law of nature. Those 112 hours of downtime are a good reason to make that choice again.

Start small. Stand up a Forgejo instance this weekend for one project, attach that .NET workflow, and push to both. If after a month you like it, move the rest in. If you decide it's not for you, you've spent one evening learning how self-hosted Git works and lost nothing.

Want to stay updated?

Subscribe to my newsletter or get in touch for freelance projects.

Get in Touch