Migrating from GitHub to Codeberg (Forgejo/Gitea)
I was always a fan of diversity in open source software and hosted offerings. It’s partially why I run my own e-mail server. With the advent of using other people’s code to train your own AI models and deteriotating relations across the Atlantic, I have decided to foster diversity by moving my code off of GitHub.
My new home for private projects is Codeberg, a Forgejo instance (which is a fork of Gitea) hosted by a non-profit organization in Berlin, Germany. It brings all the advantages of Gitea (i.e. relatively broad support in tools like goreleaser) with the benefit of adhering to German law. Most of what I describe here is therefore applicable to moving to any Gitea instance.
Thankfully, kind of, I have no big following on GitHub and there are basically no issues or pull requests in any of my repositories. If there were, Codeberg could import them, but I would surely lose a lot of previous contributors due to the friction of having “yet another platform”.
What is there to migrate?
I have mostly open source Go tools on GitHub, so my migration will have to focus on the following aspects:
- Updating my Go vanity URL for
go.xrstf.de
to Codeberg. Thankfully I have set up this one a long time ago and so folks importing my code won’t have to change anything. - Updating the goreleaser logic to work with Codeberg. Since Codeberg is Gitea, this should be easy.
- Finding a replacement for GitHub’s free CI (GitHub Actions). Someone needs to build and publish my binaries whenever I set a Git tag.
Table of Contents
- Moving the code
- Update your code
- Update your vanity Go module URLs
- goreleaser
- GitHub Actions
- Finishing up
1) Moving the code
This is the easy part. Just sign up on your favorite Gitea instance and create a new repository. Choose to migrate your existing GitHub repository:

I personally did not bother to input a token and import issues/PRs.
2) Update your code
You probably have some referenfes in your README file regarding GitHub. Now is a
good time to update those and also to update your git remotes in your local
working copy, so origin
now points to Codeberg.
git remote set-url origin git@codeberg.org:owner/project.git
git remote add github git@github.com:owner/project.git
git fetch github
Now you should be able to, during the migration, keep Codeberg and GitHub separate.
You can now set a new Git tag in your repo, which will be helpful in testing the changes from the next section of this guide.
3) Update your vanity Go module URLs
I use go.xrstf.de
as the vanity domain for all my Go projects, and
vangen to create the necessary HTML pages. To
move a repository from GitHub to Codeberg, a little bit more configuration is
necessary since my vangen fork doesn’t (yet) support Codeberg directly.
repositories:
# old (GitHub) style configuration
#- prefix: stalk
# main: true
# url: https://github.com/xrstf/stalk
# change it to a more explicit configuration for Codeberg
- prefix: stalk
main: true
type: git
url: https://codeberg.org/xrstf/stalk
source:
home: "https://codeberg.org/xrstf/stalk"
dir: "https://codeberg.org/xrstf/stalk/tree/main{/dir}"
file: "https://codeberg.org/xrstf/stalk/blob/main{/dir}/{file}#L{line}"
Run vangen
to re-generate your public/
folder.
Since Go/Google’s GOSUMDB will have probably cached the URLs for all existing tags in your repository, you might need to tag a new version (potentially even on the same commit as your last version) on Codeberg so you can force Go to re-find your code.
Important: Before you now go ahead and try to go get
your code from your new
location, absolutely make sure that the metatags in your new HTML files are correct.
They should look like this:
<meta name="go-import" content="go.xrstf.de/stalk git https://codeberg.org/xrstf/stalk">
<meta name="go-source" content="go.xrstf.de/stalk https://codeberg.org/xrstf/stalk https://codeberg.org/xrstf/stalk/tree/main{/dir} https://codeberg.org/xrstf/stalk/blob/main{/dir}/{file}#L{line}">
If this is invalid, fix it before trying to use it. Go’s (or better Google’s)
GOSUMDB will cache anything and everything immediately and forever. If your HTML
is broken and you do go get go.example.com/mytool@v1.2.3
, Google will forever
cache the broken configuration and version 1.2.3 is now effectively burned.
4) goreleaser
I use goreleaser to build and upload binaries whenever I set a Git tag in my repositories. Thankfully, since everything is Gitea under the hood, very few changes are necessary to switch to Codeberg.
In your .goreleaser.yaml
, simply add
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
You now also need to convince goreleaser to switch to Gitea, because if you use
GitHub Actions (or even, as we will see further down, Forgejo Actions), there
will always be a predefined GITHUB_TOKEN
environment variable. To switch to
Gitea mode, define a GITEA_TOKEN
and then tell goreleaser to use it. In a
workflow file (.github/workflows/
or .forgejo/workflows
), this could look
like this:
jobs:
goreleaser:
steps:
# ...
# checkout, setup, yadda yadda yadda
# ...
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --clean
env:
# These two variables flip goreleaser into Gitea mode.
GORELEASER_FORCE_TOKEN: gitea
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
That’s it already! :-)
5) GitHub Actions
Microsoft is rich and can provide free CI resources to the world. Codeberg is not and so CI functionality is not granted to every user by default. If you want to use Forgejo Actions on Codeberg, you will have to apply for it. Even if you want to bring your own runner, you need to be approved for the features to even be enabled in your account.
Head over to https://codeberg.org/Codeberg-e.V./requests and create a new ticket, then follow the ticket template.
For me the approval process took a few hours only.
Once you are approved, you’re not quite there yet. You will need to enable the actions for each repository that you want to use them in. Head to the repository settings page and click on “Units” > “Overview” in the menu.

Enable the checkboxes for releases and actions, and if you plan on hosting packages like container images, enable that checkbox, too.
Now it’s time to migrate your GitHub workflows to Forgejo workflows. Thankfully, even though Forgejo very clearly states:
The syntax and semantics of the workflow files will be familiar to people used to GitHub Actions but they are not and will never be identical.
for me and my simple workflows, I barely had to make any changes.
Simply mv .github .forgejo
and then edit your workflows. The only change I had
to make was to use full URLs for most of the actions. On GitHub, where the actions
originate, you can just refer to them using goreleaser/goreleaser-action@v6
.
This only works for some actions (those mirrored into Forgejo, like
actions/checkout
and actions/setup-go
), but for all others simply prepend
https://github.com/
:
- name: Run GoReleaser
uses: https://github.com/goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --clean
env:
GORELEASER_FORCE_TOKEN: gitea
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run golangci-lint
uses: https://github.com/golangci/golangci-lint-action@v6
with:
version: latest
That’s it. Commit, push, and (depending on your workflows) already observe how
they work. You might need to adjust the runs-on
value if you intend to use
the public Codeberg infrastructure. You can see a list of runners and their
labels in your Codeberg settings.
5.1) Running your own CI Runner
If you have a server and some capacity to spare, you can also run your own runner for your projects on Codeberg (it’s not public for everyone, it will only work for your own repositories).
The Forgejo Runner is open source (duh) and can be run in a variety of ways. I chose docker-compose and setup was quite straight forward.
Note: I tried to follow the register
container in the docker-compose example, but was confused by the shared secret, which
only ever makes sense if you also run your own Forgejo. Instead, I followed
the Codeberg documentation and
ran the appropriate commands manually.
My compose.yaml
for the runner on my server is:
version: "3.8"
# based on https://code.forgejo.org/forgejo/runner/src/branch/main/examples/docker-compose/compose-forgejo-and-runner.yml
services:
docker:
image: code.forgejo.org/oci/docker:dind
restart: unless-stopped
hostname: docker
privileged: true
environment:
DOCKER_TLS_CERTDIR: /certs
DOCKER_HOST: docker
volumes:
- docker_certs:/certs
networks:
- runner
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "10"
runner:
image: code.forgejo.org/forgejo/runner:6.2.2
restart: unless-stopped
command:
- forgejo-runner
- --config
- /etc/forgejo/config.yaml
- daemon
networks:
- runner
environment:
DOCKER_HOST: tcp://docker:2376
DOCKER_CERT_PATH: /certs/client
DOCKER_TLS_VERIFY: "1"
volumes:
- ./config:/etc/forgejo
- /srv/forgejo-runner-data:/data
- docker_certs:/certs
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "10"
# https://docs.codeberg.org/ci/actions/
runner-register:
image: code.forgejo.org/forgejo/runner:6.2.2
networks:
- runner
environment:
DOCKER_HOST: tcp://docker:2376
volumes:
- /var/forgejo-runner-data:/data
user: 0:0
# Here I just fiddled with the commands and re-ran the container a few times
# to get the necessary .runner and config.yaml files.
command: >-
bash -ec '
#forgejo-runner register --no-interactive --token "YOUR_CODEBERG_REGISTRATION_TOKEN_HERE" --name YOUR_RUNNER_NAME --instance https://codeberg.org;
forgejo-runner generate-config;
cat .runner
'
networks:
runner: ~
volumes:
docker_certs: ~
Make sure you chown the home directory for the runner (/var/forgejo-runner-data
in my case) so that the runner can create files in it.
Once you ran the commands above, your runner should already show up on Codeberg:

5.2) Enabling Docker Builds
The configuration above works fine for building Go, but when you want to build Docker images, you need to make sure the steps in your Forgejo Actions talk to the docker-in-docker daemon.
To achieve this, we need to mount the Docker PKI into each action container.
Edit the config.yaml
from your Forgejo Runner and make the following changes:
# This snippet only shows the changes that need to be made, not the entire config.yaml.
runner:
# Set environment variables to point any docker command inside an action step
# to the docker-in-docker daemon.
envs:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS: yesplease
DOCKER_CERT_PATH: /certs/client
container:
# Make action containers use the dind host network.
network: host
# mount the Docker PKI
options: "--volume /certs:/certs"
# allow the mount above
allowed_volumes:
- /certs
Once the configuration is updated, restart the runner.
To now use Docker in your actions, add these steps to your workflow:
jobs:
example:
# enable permissions
permissions:
packages: write
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker CLI
run: apt-get update && apt-get -qy install docker.io
env:
DEBIAN_FRONTEND: noninteractive
- name: Docker Login
uses: docker/login-action@v3
with:
registry: codeberg.org
username: ${{ github.actor }}
# The default token generated by Forgejo will not grant write permissions
# for package repositories; you need to manually create a token in your
# user settings, give it read&write permissions on package&repository
# (in Forgejo: User Settings > Applications).
#
# Then create a new Runner secret (User Settings > Actions > Secrets)
# and store the newly created token there, named for example "REGISTRY_TOKEN".
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Context for Buildx
run: |
docker context create builders
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
endpoint: builders
# From here on, steps can use Docker buildx to build multiarch container images.
Many thanks to Dan who helped me figure all this out :-)
7) Finishing up
You can now archive the original repository on GitHub (maybe even make one last extra commit in it to inform users about the migration to Codeberg) and then you’re already done!