Today is the day I finally made the move.

I am leaving GitHub to host my own Forgejo instance, available here.

Why?

It might seem weird to move away from GitHub at first. I mean, it’s free, has a ton of features, a strong community, good SEO, free CI/CD… Why would you want to move away from that?

There are 3 reasons why I’m moving off GitHub.

I. Because we should all own our data.

When we upload things to the internet, do we really know where they go? (This sentence looks like some conspiracy of some sort, but I swear it’s not)

The sad reality is that when we upload code on GitHub, we don’t know where it goes. What do they do with it? Where is it stored? I’m tired of trusting platforms blindly, especially big companies.

In October 2018, GitHub was purchased by Microsoft for 7.5 billion dollars. People at the time were making jokes about GitHub being full of pop-up ads and becoming an overall bad service. But Microsoft played fair, promising to keep the platform open and independent:

GitHub will retain its developer-first ethos, operate independently, and remain an open platform.

- Microsoft completes GitHub acquisition, official Microsoft Corporate blog

But since August 11, 2025, this is no longer the case, as GitHub joined the Microsoft “CoreAI” team after the former CEO Thomas Dohmke announced he would step down from his role. Which led to Microsoft absorbing the company.

So it is more important than ever to migrate.

II. Because I don’t want my code to be used to train LLMs

Microsoft has expressed multiple times their wish to have the most performant “code assistant”. To do so, they need code to train it, which just happens to be stored right on GitHub: more than 1 billion repositories, just waiting to have their code stolen to train A.I., without your consent of course. And it’s not because your repositories are private that they just won’t train on your code, trust me.

I simply do not want to contribute to the generation of AI slop, and you should know by now that I’m very anti-AI.

III. Because of what’s happening in the US

Do I want my code to be stored in the United States? Not really considering what has been happening since January 2025 and the Trump policies. We never know what could happen, especially with important data like this. Let’s be real, the US are not really interested in having my code in particular, but they have most of the code on the planet. GitHub is the most popular platform, and code is very very valuable.

It’s official, it will be Forgejo!

What’s Forgejo?

Forgejo is a self-hosted and open source alternative to GitHub. It is a fork of Gitea, and they offer the same features. The problem with Gitea is that they’ve been drifting towards “for-profit” rather than “for the community”. In 2022, a for-profit company called “CommitGo, Inc” was created for Gitea, alongside paid plans, which sparked some concerns. They are also using AI to generate images for their home page, which (appart from being ugly) is a big mistake that doesn’t fit with the open source community.

So Forgejo was made to be truely open source and free forever. It has been pushed as the one of the best alternative to GitHub by the community.

Why not Gitlab?

Hosting gitlab is honestly just pain. It is great for corporates with huge hardware resources, that need to have a lot of advanced features, but as soon as you need to host it yourself, you’ll realize that it’s far from lightweight (we’re talking more than 4gb of RAM at IDLE without optimizations, 2.5gb with optimizations) and it’s full of features you will most likely never use anyway.

GitLab is also made for-profit first, which means their official Gitlab instance has more updates, more features, and pushes you to purchase a plan, which again doesn’t really fit with my convictions.

So I’ve decided to go with Forgejo. It’s lightweight, faster, and made in Go.

Installation of Forgejo

Setting up the forge

To deploy Forgejo, I used docker-compose, as it is (in my opinion) the best option to do so.

I’ve simply followed their installation instructions, and use the given docker-compose.yml with PostgreSQL as a database. The installation is pretty straightforward, as it’s pretty much done already. A quick docker compose up -d and your Forgejo instance is waiting for you on port 3000.

Setting up a reverse proxy

Accessing an IP with a port in plain HTTP is not pretty at all and of course should be avoided at all costs.

So, to handle certificates and the reverse proxy, I fell in love with Caddy a while back. It’s fast, easy and straight-forward. I have one Caddy server and it works like a charm. Never had any issues!

If you are interested, here is my configuration:

git.ctrlalt.cat {
	reverse_proxy http://localhost:3000
}

That’s it, Caddy will handle the SSL certificate generation for you. We now have a fully working Forgejo instance!

SSH issues

While installing my instance, I’ve stumbled upon ssh issues, because I want to run my ssh server on port 2222 to avoid nuking the ssh server I use to access the host.

You have to manually specify your custom port in the gitea config file available at ./data/gitea/conf/app.ini:

[server]
SSH_PORT = 2222
SSH_LISTEN_PORT = 2222

What about CI/CD?

As I’ve said earlier, one great thing about GitHub is the “GitHub Actions” feature. It’s free, easy to use, and makes you avoid hosting your own CI/CD suite. Personally, it was a must for me, so I needed to take that into consideration while chosing my GitHub alternative.

Luckily Forgejo has the same feature, Forgejo Actions, that you can self-host. It has the same yml syntax than GitHub Actions, but uses the .forgejo/workflows directory rather than .github/workflows.

Hosting a runner

For this feature to work, you need to host at least one “actions runner”.

To do so, I followed their official guide, and used Docker. Do not install it as a binary on your server without virtualization, because your runner will have access to your server, and it can end up pretty badly.

Personally, I’ve chosen to host it with docker-compose again. To make things easier, I’ve decided to integrate it to my already existing docker-compose.yml made for my Forgejo instance.

Issues with Docker

After installing the runner for the first time, I’ve had issues with actions failing because it couldn’t connect to the docker socket to pull images.

This is because you need to host a “Docker in Docker” container alongside your runner. You can find that configuration below in the final docker-compose.yml file.

Issues with directory ownership

I needed to manually apply ownership to the runner-data directory:

chown -R 1001:1001 runner-data/

Registering the runner

To use the runner with forgejo, we need to register it. To do so, we need to bring both our instance and our runner up using docker compose up -d. By default, you will notice that the command associated with the Forgejo runner in the docker-compose.yml file is:

/bin/sh -c "while : ; do sleep 1 ; done ;"

This is because we need to configure it first before starting the daemon. To configure it, all we have to do is run docker exec -it runner /bin/sh (considering your runner container is called runner). Once in the container, we need to run forgejo-runner register and follow the instructions. To get your runner token, go to your instance, click on your profile picture > administration > actions > runners > add a new runner.

Starting the runner

Once the runner has been configured, we can exit the runner’s shell. Then, we need to change the runner container command (I’ve talked about it above) to make the it actually start with the container. The official documentation gives us the following command:

/bin/sh -c "sleep 5; forgejo-runner daemon"

We can save the docker-compose.yml file and recreate the runner container with docker compose up -d. You might notice that the runner takes a long time to stop at first.

The runner is ready!

Migrating repositories from GitHub to Forgejo

One amazing thing about Forgejo is that there is a built-in repository mirroring solution. If you want to keep your GitHub account and just use your forgejo instance as a backup, you can do that. If you just want to clone already existing repositories from GitHub to your forgejo instance, you can without losing your commit history, issues, etc. This is another reason as of why I’ve decided to not go with Gitlab, because in Gitlab, this feature is hidden behind a paywall and is not available for self-hosted instances.

Unfortunately, you cannot import repositories in bulk in Forgejo. Luckily, I came accross this article by Aaron Yarborough about his migration to Forgejo. He made a small Node script that uses GitHub CLI and the Forgejo API to import repositories in bulk, I’ve made a few tweaks to it but they are not worth to mention. Go check it out if you’re interested!

Backups

To backup my instance, I’ve decided to create a restic repository on an external storage option. Of course, you should set-up backups too!

What next?

I still need to delete my repos of GitHub and migrate my GitHub actions to forgejo.

Also, I rely a lot on CloudFlare Pages, and CloudFlare Pages only supports GitHub and the official Gitlab instance for automatic builds. I guess I’ll have to make my own action.

Full docker-compose.yml file

Here is the full docker-compose.yml file for my install:

networks:
  forgejo:
    external: false

services:
  server:
    image: codeberg.org/forgejo/forgejo:12
    container_name: forgejo
    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=XXXXXXXXXXXXXXXXXXXXX
      - GITEA_CUSTOM=/data/gitea # not required, must be provided if you plan to install forgejo themes or customize the interface yourself
    restart: unless-stopped
    networks:
      - forgejo
    volumes:
      - ./data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - '3000:3000'
      - '2222:22'
  docker-in-docker:
    image: docker:dind
    container_name: 'docker_dind'
    privileged: 'true'
    command: ['dockerd', '-H', 'tcp://0.0.0.0:2375', '--tls=false']
    restart: 'unless-stopped'

  runner:
    image: 'data.forgejo.org/forgejo/runner:4.0.0'
    links:
      - docker-in-docker
    depends_on:
      docker-in-docker:
        condition: service_started
    container_name: 'runner'
    environment:
      DOCKER_HOST: tcp://docker-in-docker:2375
    user: 1001:1001
    volumes:
      - ./runner-data:/data
    restart: 'unless-stopped'

    command: '/bin/sh -c "sleep 5; forgejo-runner daemon"'
  db:
    image: postgres:latest
    restart: unless-stopped
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=XXXXXXXXXXXXXXXXXXXXX
      - POSTGRES_DB=forgejo
    networks:
      - forgejo
    volumes:
      - ./postgres:/var/lib/postgresql/data