Source control is really great, and very accessible today. Many developers starts with just a pile of files and no source control, but you soon have a disaster and wish you had a better story than a bunch of copies of your files. In the early days of computing, source control was a lot more gnarly to get setup – now we have things like GitHub that make it easy.
Forgejo brings a GitHub like experience to your homelab. I can’t talk about Forgejo without mentioning Gitea. I’ve been running Gitea for about the last 7 years, it’s fairly lightweight and gives you a nice web UI that feels a lot like GitHub, but it’s self-hosted. Unfortunately there was a break in the community back in 2022, when Gitea Ltd took control – this did not go well and Forgejo was born.
The funny thing is that Gitea orginally came out of Gogs. The wikipedia page indicates Gogs was controlled by a single maintainer and Gitea as a fork opened up more community contribution. It’s unfortunate that open source projects often struggle either due to commercial influences, or the various parties involved in the project. Writing code can be hard, but working with other people is harder.
For a while Forgejo was fully Gitea compatible, this changed in early 2024 when they stopped maintaining compatibility. I only became aware of Forgejo in late 2024, but decided Gitea was still an acceptable solution for my needs. It was only recently that I was reminded about Forgejo and re-evaluated if it was finally time to move (yes, it was).
Forgejo has a few installation options, I unsurprisingly selected the docker path. I opted to use the rootless docker image which may limit some future expansion such as supporting actions, but I have basic source control needs and I can always change things later if I need.
My docker-compose.yml uses the built in sqlite3 DB, but as mentioned above is using the rootless version which is a bit more secure.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
networks: forgejo: external: false services: server: image: codeberg.org/forgejo/forgejo:14-rootless container_name: forgejo user: 1000:100 environment: - USER_UID=1000 - USER_GID=100 restart: always networks: - forgejo volumes: - ./forgejo:/var/lib/gitea - ./conf:/etc/gitea - /etc/localtime:/etc/localtime:ro ports: - '3001:3000' - '222:2222' |
As I’m based on NixOS, my gid is 100, not the typical 1000. I had to modify my port mapping to avoid a conflict with Gitea which is already using 3000.
Now, the next thing I need (ok, want) to do is configure my nginx as a reverse proxy so I can give my new source control system a hostname instead of having to deal with port numbers. I actually run two nginx containers – one based on swag for internet visible web, and another nginx for internal systems. With a more complex configuration I could use just one, but having a hard separation gives me peace of mind that I haven’t accidentally exposed an internal system to the internet.
I configured a hostname (forge.lan) in my openwrt router which I use for local DNS. My local nginx is running with a unique IP address thanks to macvlan magic. If I map forge.lan to the IP of my nginx (192.168.1.66) then the nginx configuration is fairly simple, I treat it like a site creating a file config/nginx/site-confs/forge.conf that looks like:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# Git server (Forgejo) - https://forgejo.org/docs/latest/admin/setup/reverse-proxy/#basic-http server { listen 80; # Disable IPV6 # listen [::]:80; server_name forge.lan; location / { proxy_pass http://192.168.1.79:3001; proxy_set_header Connection $http_connection; proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 512M; } } |
Most of this is directly from the forgejo doc on reverse proxies. When my nginx gets traffic on port 80 for a server named forge.lan, it will proxy the connection to my main server (192.168.1.79) running the forgejo container.
With this setup, we can now start the docker container
|
1 |
docker compose pull; docker compose up -d |
And visit http://forge.lan to be greeted by the bootstrap setup screen. At this point we can mostly just accept all the defaults because it should self detect everything correctly.
When we interact with this new self hosted git server, at least some of the time we’ll be on the command line. This means we’ll be wanting to use a ssh connection so we can do things like this
|
1 2 |
# Check out our NixOS configurations git clone ssh://git@forge.lan:222/myuser/nixos-configs.git |
There is a problem here. If you recall the webserver (192.168.1.66) is not on the same IP as the host (192.168.1.79) of my forgejo container. Since I want the hostname forge.lan to map to the webserver IP, I’ve introduced a challenge for myself.
When I hit this problem with Gitea, my solution was simply to switch to using my swag based public facing webserver (which runs on my main host IP) and use a deny list to prevent anyone from getting to gitea unless they were on my local network. This works, but means I had some worry that one day I’d mess that up and expose my self hosted git server to the internet. It turns out there is a better way, nginx knows how to proxy ssh connections.
This stackexchange post pointed me in the right direction, but it’s simply a matter of adding a stream configuration to your main nginx.conf file.
|
1 2 3 4 5 6 7 8 9 |
stream { upstream ssh { server 192.168.1.79:222; } server { listen 222; proxy_pass ssh; } } |
After restarting nginx, I can now perform ssh connections to forgejo. This feels pretty slick.
I then proceeded to clone my git repos from my gitea server to my new forgejo server. This is a bit repetitive, and to avoid too many mistakes I cooked up a script based on this pattern. Oh yeah, and I did need to enable push-to-create on forgejo to make this work.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#!/usr/bin/env bash echo "clone repo: " $1 dir=`echo "${1##*/}"` echo " directory: " $dir read -p "Press enter to continue OR CTRL-C to abort" git clone --mirror $1 cd $dir git remote set-url origin ssh://git@forge.lan:222/myuser/$dir git push --mirror origin cd .. rm -rf $dir |
This script takes a single parameter which is the URL for the source (Gitea) server. It then strips off the project name which is the directory of the project. We are using the --mirror option to tell git we want everything, not just the main branch.
Using this I was able to quickly move all 29 repositories I had. My full commit history came along just fine. I did lose the fun commit graph, but apparently if you aren’t afraid to do some light DB hacking you can move it over from Gitea as the formats are basically the same. The action table is the one you want to move / migrate. I’m ok with my commit graph being reset.
You also don’t get anything migrated outside of the git repos, this means issues for example will be lost. For me this isn’t a big deal, I had 3 issues all created 4 years ago. If you want a more complete migration you might investigate this set of scripts.
The last thing for me is to work my way through any place I’ve got any of those repositories checked out, and change the origin URL. For example consider my nix-configs project
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ cd nix-configs # check the current repo $ git remote -v origin ssh://git@mygitea.lan:3022/myuser/nixos-configs.git (fetch) origin ssh://git@mygitea.lan:3022/myuser/nixos-configs.git (push) # change to the new server $ git remote set-url origin ssh://git@forge.lan:222/myuser/nixos-configs.git # verify we have it all updated $ git remote -v origin ssh://git@forge.lan:222/myuser/nixos-configs.git (fetch) origin ssh://git@forge.lan:222/myuser/nixos-configs.git (push) |
At this point I’m fully migrated, and can shut down the old gitea server.
If you were interested in a Forgejo setup that has actions configured and is setup for a bit more scale, check out this write up.