I wanted to leave GitHub when Microsoft bought it in 2018. But I never actually got around to doing it.
I wanted to leave GitHub when Copilot launched in 2021 and they helped themselves to public repositories for training data. But I never actually got around to doing it.
So when the AI driven traffic increase took the platform down repeatedly in 2026, I had wanted to leave anyway. I shopped around a bit, joined Codeberg e.V. and moved everything over.
Text (for humans and machines)
I used the opportunity to drop some old repositories I didn’t want to keep. Since I use git-bug for my own issues, it’s just included when the git repo is migrated. The rest I moved either manually or using their migration tool. Most of my projects don’t have many external contributors, so nothing to migrate.
Continuous Integration
What gave me some trouble is migrating from GitHub Actions to the Codeberg hosted Forgejo Actions.
I was not able to build multi-arch Docker images on CI.
When building an arm64 image on an amd64 host, you need to emulate any RUN instructions in the Dockerfile.
Under Linux, the binfmt module can register a specific emulator for a specific binary executable format.
For example, use qemu-arm-static for an arm64 binary.
Docker and Podman both expect this, but Codeberg doesn’t allow the registration.
Forgejo Actions are pretty compatible with GitHub Actions, so sometimes you can just continue using them.
setup-beam is one such action to set up an Erlang/Elixir environment.
To download pre-build binaries, it looks at the case-sensitive ImageOS env variable set by the GitHub runner to ubuntu24.
Codeberg sets it to codebergtiny - no pre-build binaries available.
Just override it, right?
Well, that changes the variable name to snake_case, so ImageOS becomes IMAGE_OS; the value is not overridden.
Besides these Issues, I got most of my (very limited) CI working. Except for building multi-arch Docker images - I do that manually on my machine and push the results now.
I can live with these restrictions, as well as the slower startup times and tight deadlines for actions. Figuring this out however took some iteration, which is painful when you have to commit-push-wait-repeat for each change. This was already terrible with GitHub actions, now it is terrible with Forgejo Actions.
Docker Images
The automatic Codeberg migration tool does not migrate Docker images from the gchr.io container registry.
The simple option is to just use skopeo copy.
This works, but doesn’t update the images labels, so they won’t show up under the repositories “Packages” tab. If you do want the image to correctly show up, you need to do it manually.
๐I did this on my Bazzite machine, using Podman. If you use Docker, substitute
podmanwithdockerand everything should work the same.
First, pull the images for all platforms it supports:
$ podman pull --platform linux/amd64 ghcr.io/lukasknuth/briefly
$ podman pull --platform linux/arm64 ghcr.io/lukasknuth/briefly
# Pull any additional platforms...
These images then show up in the local image list.
The first one that was pulled will have the tag set, all others will show up as <none> - this is expected:
$ podman images
REPOSITORY TAG IMAGE ID
ghcr.io/lukasknuth/briefly latest cafe1234
<none> <none> affe5678
If you want to make sure they’re correct, you can inspect the image. This will also reveal information about target OS and architecture:
$ podman inspect cafe1234
# Look for .Config.Entrypoint or .Labels to identify the image
$ podman inspect cafe1234 | jq '.[] | .Os + "/" .Architecture'
"linux/amd64"
Once all images are pulled locally, add or update the org.opencontainers.image.source label.
This is used by Forgejo to tell which image belongs to which repo.
If your images were previously built using the standard GitHub Docker CI actions, they will have the label set to a https://github.com/.. URL.
Check the manifest as above to validate.
Since images are immutable, we can’t simply change the label. Instead, we’ll build a new tiny layer on top that adds/overrides it:
# NOTE: Use the Image ID directly here
echo "FROM cafe1234" | podman build --label org.opencontainers.image.source="https://codeberg.org/LukasKnuth/briefly" -
# Repeat for all images that are part of the multi-arch image
Mind the - at the end of the podman build command; it instructs podman to read the Dockerfile from stdin.
For extra points, you can also update the org.opencontainers.image.url label with the same value so that it doesn’t point to GitHub anymore.
Alternatively, use --inherit-labels false to drop all labels except the ones specified by the build command.
Now that all individual images are updated, we want to combine them to a single multi-arch one.
To do this, create a new manifest with the correct tag for codeberg.org and add each individual image to it:
# NOTE: If no tag is specified, ":latest" is used implicitly
$ podman manifest create codeberg.org/lukasknuth/briefly:0.1.3
# Verify that there is one new manifest with a new Image ID
$ podman images --filter manifest=true
REPOSITORY TAG IMAGE ID
codeberg.org/lukasknuth/briefly 0.1.3 beef9012
# Find all individual images to add to the manifest
$ podman images --filter label=org.opencontainers.image.source="https://codeberg.org/LukasKnuth/briefly"
REPOSITORY TAG IMAGE ID
<none> <none> cafe1234
<none> <none> affe5678
# Add all of them
$ podman manifest add codeberg.org/lukasknuth/briefly:0.1.3 cafe1234
$ podman manifest add codeberg.org/lukasknuth/briefly:0.1.3 affe5678
# Verify individual image IDs and platform os/arch
$ podman manifest inspect codeberg.org/lukasknuth/briefly:0.1.3
The manifest itself is just the list of images, all of which exist locally. The last step is to push everything to Codeberg:
$ podman manifest push --all codeberg.org/lukasknuth/briefly:0.1.3
This uploads all individual images and the manifest, linking them together into a multi-arch image. You can then verify that the image shows up in the “Packages” tab of your repository. On the detail page, the “Images” table should show one entry for each os/arch pair that the multi-arch image supports.
Pages
After my struggle with the CI, publishing static websites to Codeberg Pages was surprisingly easy. As a result, this website as well as my pacman game are now hosted there.
Outlook
Codeberg has made some headlines with its recent decision to not allow LLM driven projects on its platform anymore. While this aligns with my views on the technology, I also appreciate differing opinions for what this decision might imply about the future. My active Codeberg e.V. membership will give me some influence at least.
Right after I completed my migration, I was made aware of Terms of use ยง2.1 which states (emphasis mine):
Public repository content shall be made available under a copyright licence which gives all natural and legal persons the following rights in the content:
- the right to use the content for any purpose, including both commercial and non-commercial purposes;
To the extent that one can realistically prevent it, I’d love the option to not allow corpos to make money off my hobby projects. Jokes on me for not reading the (pretty short) document before migrating. I haven’t decided whether this is an issue for me yet. But I might migrate some selected repos to a fully private setup. Eventually. Surely…