TL;DR
- Moved self-hosted source control from Dockerized GitLab CE on the Debian server to Forgejo on the Talos Kubernetes cluster.
- Forgejo runs as a single pod with a SQLite database on a 10 GiB Longhorn PVC. No Postgres.
- Exposed via Cilium Gateway API with separate listeners for HTTPS and SSH, each with its own LoadBalancer IP from the Cilium L2 pool.
- TLS from cert-manager plus Let’s Encrypt, with Gateway API support explicitly enabled. ExternalDNS writes Cloudflare records from the Gateway resources.
- Full repo migration, about a month of parallel running, then GitLab CE was shut down.
What I built
Forgejo lives in the forgejo namespace on the Talos cluster. It is a single pod with a SQLite backend. The database file sits on a 10 GiB Longhorn PVC named gitea-shared-storage, mounted at /data/gitea. App config is at /data/gitea/conf/app.ini with DB_TYPE = sqlite3. There is no Postgres service or Secret in the namespace.
Traffic comes in through Cilium Gateway API. I set up separate listeners for HTTPS and SSH under the same private hostname behind the Cilium Gateway. Each listener gets its own LoadBalancer IP from the Cilium L2 pool, so HTTPS and SSH are pinned to stable addresses. cert-manager handles TLS via Let’s Encrypt, and ExternalDNS watches the Gateway resources to update Cloudflare automatically.
Why move off GitLab CE
I completely stopped using GitHub.com for anything that I cared about. Self-hosting the repositories that matter was the obvious next step; the only question was which self-hosted instance.
I love GitLab. I like self-hosting GitLab. However, self-hosting GitLab was overkill for my use case and was taking up resources on my Debian server that I needed for other priorities. Forgejo checked all the boxes of things I need, and much less overhead.
The migration
I deployed Forgejo on 2026-03-12. The web UI was exposed through Cilium Gateway API and the Let’s Encrypt cert issued the same day. Admin login was set up, but repos were still in GitLab CE.
Between 2026-03-12 and 2026-03-29 I exported every repo from GitLab CE and imported into Forgejo. This was not a fresh start. On 2026-03-29 I fixed DNS so the private hostname resolved to the Cilium LoadBalancer pool, then repointed Obsidian vault backups to Forgejo SSH. The launchd job for vault backups is now functional.
I kept GitLab CE running for about a month as a fallback. Maybe not even a full month, and didn’t run into any issues where I needed to fallback. Around April 2026 I shut the GitLab CE container down on the Debian server.
What Cilium Gateway API gave me
The Forgejo deployment exposes two network surfaces, each with its own LoadBalancer IP from the Cilium L2 announcement pool (192.168.2.200-230).
The HTTPS surface uses Gateway API. A Gateway defines an HTTPS listener on port 443 with hostname git.<my-domain>.net, with TLS terminated by a cert-manager-issued Let’s Encrypt certificate. An HTTPRoute pinned to that listener routes traffic to the Forgejo Service. A second HTTPRoute on the Gateway’s port 80 listener redirects HTTP to HTTPS. ExternalDNS reads the HTTPRoute’s hostnames field and creates thec Cloudflare A record for git.<my-domain>.net.
SSH is a separate Service of type LoadBalancer at git-ssh.<my-domain>.net. Port 22 maps to Forgejo’s SSH daemon on targetPort 2222. ExternalDNS reads the external-dns.alpha.kubernetes.io/hostname annotation on the Service to create the A record.
I also get explicit IP allocation from a pool. I defined the LoadBalancer IP range Cilium can hand out, so HTTPS and SSH each got a stable, predictable address from the pool.
What I learned
What I learned? That moving to Forgejo was dead simple, and I forgot that I even had Gitlab running until I needed those resources.