---
title: Migrating from GitHub to Forgejo
pubdate: 2025-03-16T00:00:00.000Z
desc: I recently moved all of my reposfrom GitHub to a self-hosted Forgejo instance running on my own servers.
tags: 
    - tech
    - hosting
---

I recently moved all of my repos (public and private) from GitHub to a self-hosted [Forgejo](https://forgejo.org/) instance running on my own servers.

There were a few reasons for this:
- I believe it's important to own and control my own data
- I do not want my private repositories to be used to train LLMs
- In protest of the current (2025) USA administration, I am reducing my usage and reliance on US products and services

Hopefully this post can serve as a guide for anybody who wants to do the same!

## The setup

My overall setup looks like this:
- Ubuntu VPS in Germany, hosted with [Hetzner](https://www.hetzner.com/) (a German VPS provider)
- A Forgejo instance running on Docker
- [Caddy](https://caddyserver.com/) as a reverse proxy routing `git.aaronjy.me` to the Forgejo Docker container

## The process

The overall process was pretty simple:
1. Get a list of all of my public and private repos
2. Use a script to call Forgejo's `/api/repos/migrate` endpoint to copy the repo from GitHub to Forgejo
3. Delete the repo on GitHub

### Step 1 - Get a list of all my repos

I used the [GitHub CLI](https://cli.github.com/) for this, using the `gh repos list` command. I wanted to move my private repos across first, so I wrote two commands: one for private repos, and one for public ones. Both commands write the JSON output to a respective JSON file.
```sh
# Get all private repos
gh repo list --visibility=private --json id,name,owner,sshUrl,url --limit 200 > gh-private-repos

# Get all public repos
gh repo list --visibility=public --json id,name,owner,sshUrl,url --limit 200 > gh-public-repos
```

The output looks like this:
```json
[
	{
	    "id": "R_kgDOOCEBIw",
	    "name": "kielder-commerce",
	    "owner": {
	      "id": "MDQ6VXNlcjM4NTU4MTk=",
	      "login": "AaronJY"
	    },
	    "sshUrl": "git@github.com:AaronJY/kielder-commerce.git",
	    "url": "https://github.com/AaronJY/kielder-commerce"
	},
	...
]
```

## Step 2 - Use Forgejo's API to migrate from GitHub

Usefully, Forgejo has a build-in endpoint for migrating GitHub repos: `/api/repos/migrate`

All I had to do was write a script that sent each repo from the JSON to the endpoint to start the migration process, and Forgejo handles the rest.

My (nodejs) script ended up looking like this:

```js
require('dotenv').config()
const fetch = require("node-fetch");
const repos = require("./github-private-repos.json"); // <- migrate your public or private repos

const forgejoAccessToken = process.env.FORGEJO_ACCESS_TOKEN; // <- You need to generate an access token on Forgejo
if (!forgejoAccessToken) {
    console.error("Forgejo access token not set.");
    process.exit(1);
}

for (const repo of repos) {
    const reqBody = {
        clone_addr: `${repo.url}.git`,
        repo_name: repo.name,
        private: true, // <- set to `false` if migrating public repos
        service: "github",
        auth_token: process.env.GITHUB_TOKEN, // <- You need to generate a GitHub access token
        repo_owner: "aaron"// <- the name of your Forgejo user
    };

    console.log(`Migrating ${repo.name}...`);
    console.log(reqBody);

    fetch("https://git.aaronjy.me/api/v1/repos/migrate", {
        method: "POST",
        body: JSON.stringify(reqBody),
        headers: {
            "Authorization": "token " + forgejoAccessToken,
            "Content-Type": "application/json"
        }
    }).then(response => {
        response.json().then(data => {
            console.log(data);
        })

        if (!response.ok) {
            throw "Failed with status code: " + response.status;
        }

        console.log("Successfully migrated " + repo.url);
    }).catch(error => {
        console.error("Migrate API request failed: " + error);
    });
}
```

## Step 3 - Delete repos from GitHub

Once everything was moved across (and a quick sanity check was done), I looped through all of my repos in the JSON files and called the `gh repo delete` command on the GitHub CLI.
```sh
gh repo delete https://github.com/AaronJY/kielder-commerce --yes
```

## Still to do...

I still need to route SSH traffic to Forgejo's internal SSH server to allow SSH operations with repos, rather than relying on HTTPS interactions. Caddy can't be used for this as it's an HTTP proxy only, and therefore doesn't understand the SSH protocol. It might be possible to use an experimental add-on for Caddy called [caddy-l4](https://github.com/mholt/caddy-l4) that enables layer 4 proxying (on the TCP/UDP level), though it might be easier to tweak my server's IP tables to forward traffic from a custom port to the SSH port on Foregejo's Docker container.