Aller au contenu
xreveillon.eu

Je migre de Gitlab à Forgejo

En un mois, je réduis l'empreinte ressources de deux de mes services : Forgejo a absorbé à la fois la forge (GitLab) et le registre d'images conteneur. Récit d'une consolidation — le pourquoi, la migration au fil de l'eau, et les pièges rencontrés.

10 min de lecture
Terminal affichant la commande "./migrate --dry-run" et un dessin avec une flèche allant du logo gitlab au logo forgejo

En un mois, j’ai remplacé deux composants de mon homelab par un seul. Pas une forge par une autre : une forge et un registre d’images, fusionnés dans un même outil.

Ce billet raconte pourquoi, comment, et ce que la migration m’a coûté. Ce n’est pas un tutoriel pas-à-pas, plutôt un récit de migration avec les pièges au passage. Si vous avez un homelab avec des services conteneurisés, il y a des choses à en tirer.

Avant : la stack et la gourmandise

Mon homelab, c’est un serveur avec beaucoup de services conteneurisés, déployés en GitOps : la configuration vit dans un repo git, et un pipeline le déploie. Sur GitLab, un groupe dédié rassemblait ces services. C’était propre, mais ça reposait sur un noyau lourd.

En cas de crash, trois composants étaient à remonter en premier : GitLab (forge + CI), le registre d’images (qui contiennent notamment les images pour les jobs CI), et Traefik (reverse proxy). C’est ce que j’appelle le set bootstrap de la machine.

Le plus encombrant, c’était GitLab : Rails, Workhorse, Sidekiq, Gitaly, PostgreSQL (mais externe), Redis. Une petite constellation de services rien que pour héberger du code. De loin, le composant le plus lourd de la machine.

Le registre, lui, avait un passif. J’ai démarré sur Gitlab a ses débuts et le support des registres d’images conteneurs (ou même les packages des projets) etait balbutiant, j’avais alors choisi le plus simple : un simple docker registry bête et méchant. Puis j’ai voulu avoir une UI par dessus, et le service convenant le mieux à mes besoins était Artifactory JCR. Je l’ai utilisé pendant plusieurs années.

Je l’ai décommissionné le mois dernier. Les raisons réelles : c’était lourd (deux minutes à démarrer sur mon serveur), et je suis déçu depuis le début que Jfrog Artifactory ne propose pas de version gratuite « tout en un » (packages « normaux » (java-maven, go, npm, …) plus image docker : les images docker sont proposées dans un service gratuit totalement à part), et ce n’est pas open-source. J’ai un moment pensé à Nexus Sonatype OSS (maintenant Community edition), mais je n’ai jamais franchi le cap. Je l’ai remplacé par Zot, qui m’a immédiatement convaincu — vif, léger, simple. Une bonne décision, que je ne regrette pas : elle prouvait qu’un registre n’est pas forcément une usine à gaz.

Mais là, Forgejo me permet de faire d’une pierre deux coups : alléger mes services, et migrer vers une solution de forge et d’hébergement de packages tout en un.

Une raison de plus de migrer : certaines fonctions restaient verrouillées derrière les offres payantes de GitLab (Premium/Ultimate), par exemple CODEOWNERS. Dommage, pour un outil que j’hébergeais moi-même, et que je comptais utiliser comme dans un environnement professionnel, avec protection de certains fichiers (notamment .gitlab-ci.yml) lors de lancement de pipelines.

Pourquoi Forgejo

Forgejo coche des cases importantes pour moi. C’est un projet pleinement open-source, développé autour de Codeberg (association allemande à but non lucratif), publié en licence MIT intégrale — pas d’open-core, pas de version bridée pour pousser à payer. La gouvernance est démocratique, portée par le Forgejo Council.

Côté technique, le contraste est net. GitLab est un empire ; Forgejo est un binaire Go. Une empreinte de l’ordre de 150–300 Mo de RAM, contre plusieurs Go pour l’équivalent GitLab. Et comme c’est un conteneur unique, une mise à jour est très simple : comme pour GitLab, après lecture des release-notes, un simple changement de version dans le fichier .env, un git push, et le pipeline s’occupe de l’upgrade.

La compatibilité avec GitHub Actions est la cerise sur le gâteau. Forgejo Actions reprend la syntaxe de GitHub Actions. Les images officielles, le marketplace, les actions tierces : la plupart fonctionnent telles quelles. Et deuxième cerise (ça a du bon d’être gourmand) : comme GitHub est LA forge de facto, les LLM sont très bons pour écrire et réparer des workflows au format GitHub Actions. Forgejo en profite indirectement — un vrai bonus quand on on travaille beaucoup avec un LLM “assistant”.

L’argument décisif a été la consolidation. Forgejo embarque un registre de paquets et d’images conteneurs (OCI qui 0lus est !). À partir du moment où il absorbait la forge, il pouvait absorber aussi le registre. On passait de deux composants (forge + registre) à un seul. Et le set bootstrap après un crash passait de trois à deux : GitLab + registre + Traefik, puis Forgejo + Traefik.

J’ai fait une phase de test de quelques jours. Puis le bilan, début août : oui, on migre. La consolidation démarre ensuite, et suit encore son cours .

La migration

La migration se fait au fil de l’eau, pas en big-bang. Chaque repo git est migré vers Forgejo, puis la pipeline Gitlab transcrite en Forgejo Actions, à mon rythme. Pas de bascule forcée à une date imposée.

Un peu dit, la partie la plus longue, c’est la CI. Il faut réécrire chaque .gitlab-ci.yml en workflows Forgejo, dans un dossier .forgejo/workflows/ par repo git. Les syntaxes GitLab et GitHub Actions ne se ressemblent pas, et chaque type de repo a ses particularités.

Le déploiement GitOps, lui, reste intact. Le runner lance des conteneurs pour exécuter les jobs — c’est tout ce qu’il faut savoir pour la suite. Résultat : le pattern final ne bouge pas. Pour déployer un service, il suffit toujours d’un docker compose up -d dans le working directory du repo git. Le pipeline le fait, comme avant.

Le registre intégré

La partie élégante, c’est le registre. Comme chaque build pousse son image vers forgejo.xreveillon.eu/{owner}/{image}, les images que je stockais dans mon ancien registre (Zot) migrent de facto au fur et à mesure de l’exécution des pipelines, sans campagne de migration dédiée. Le registre intégré de Forgejo prend le relais de Zot de manière presque transparente (il faut tout de même que je dise à mes consommateurs d’images qu’elles ont migrées).

Un exemple concret

Cas d’école : mon image utilitaire docker/docker (Docker + Node.js + outils de build). Elle est construite par Forgejo Actions, publiée sur le registre intégré, et récupérée anonymement par les pipelines qui en ont besoin. Un build, un push, un pull : tout passe par Forgejo, rien d’autre.

Le stockage ZFS

Les volumes du conteneur Forgejo sont hébergés sur un filesystem ZFS. Le stockage est éclaté en cinq datasets ciblés (git, gitea, packages, log, ssh), chacun avec ses snapshots réguliers — des sauvegardes par composant, pas un gros bundle monolithique.

Je ne crée pas mes snapshots moi-même : j’utilise Sanoid, développé et publié par Jim Salters, avec une configuration déclarative dans /etc/sanoid/sanoid.conf :

# /etc/sanoid/sanoid.conf — snapshots réguliers sur le dataset des repos git
[zpool/forgejo/git]
    use_template = production
    hourly = 24
    daily = 7

Et pour une copie hors du pool, combiné à Sanoid, j’utilise Syncoid, de Jim Salters également et dans le même paquet apt de toutes façons, qui se charge de la réplication incrémentale :

syncoid zpool/forgejo/git remote:backup/forgejo/git

Ça respecte une partie de la règle des 3-2-1 : trois copies des données, dans au moins deux endroits différents.

Les pièges vécus

La partie la plus instructive de la migration, ce sont les pièges.

1. Les pulls anonymes : trois portes

Le registre refusait les pulls anonymes. Il y avait en fait au moins deux portes à ouvrir, et elles sont indépendantes :

  • REQUIRE_SIGNIN_VIEW=false pour que l’exploration des repos git, et la lecture du registre n’exige, pas de connexion ;
  • la visibilité de l’organisation ou du propriétaire passée à public (réglage réservé à l’admin) ; le package suit son owner/organisation, et non son projet

Une fois les deux réglées, les pulls anonymes ont fonctionné.

2. L’image runner sans Node.js

Première exécution de CI, premier échec : les actions JavaScript comme checkout@v4 échouaient immédiatement. Cause classique : l’image docker:27 (base Alpine) n’embarque pas Node.js, dont ces actions ont besoin. La solution a été mon image maison docker/docker, qui ajoute Node.js et les outils de build à l’image Docker de base.

3. Un token qui ne peut pas pousser d’image

Autre échec, message cryptique : unauthorized: reqPackageAccess. En clair : le token généré automatiquement par Forgejo ne peut pas pousser de package vers une organisation. Pour débloquer le push depuis la CI, il a fallu créer un token personnel (PAT) avec l’étendue write:package, et le déclarer en secret du repo git.

Note: Les noms de secrets ne doivent pas commencer par FORGEJO_ ni GITHUB_ (ils sont réservés). Mon premier réflexe m’a fait perdre du temps.

4. Le checkout et le réseau interne

Détail piégeux : actions/checkout clone via github.server_url, qui pointe vers l’URL interne de l’instance. Le clonage se fait donc depuis le conteneur du job, pas depuis le runner. Si ce conteneur n’est pas sur le même réseau “docker” que Forgejo, le clonage échoue. Leçon : les conteneurs de jobs doivent être sur le même réseau de travail.

5. FORGEJO__section__key écrase la configuration

Le piège qui m’a coûté le plus cher. Une variable d’environnement de la forme FORGEJO__section__key écrase l’entrée correspondante dans app.ini à chaque redémarrage. J’avais édité app.ini à la main, et mes modifications ont disparu au reboot suivant.

La source de vérité, dans mon cas, c’est compose.yaml : les variables d’environnement y sont définies. Conseil : ne touchez jamais à app.ini à la main, ou vos réglages seront écrasés au redémarrage.

Tip: Dans la plupart de ces pièges, la cause n’est pas la jeunesse de Forgejo. C’est moi qui découvrais l’outil et n’en connaissais pas encore les subtilités. La différence est importante : elle change la façon de chercher la solution.

Bilan : ce que j’ai gagné, ce que j’ai perdu

L’état des lieux :

Ce que j’ai gagné :

AvantMaintenant
EmpreinteGitLab : environ 6,5 Go combiné entre le serveur, les runners et ZotForgejo : environ 200 Mo combiné entre le serveur et les runners
Base de donnéesGitLab : 28 connexions vers mon serveur Postgres partagéForgejo : je reste sur SQLite, pas d’ambition de scaler très haut
ComposantsGitLab + Zot, deux briquesForejo, un seul composant
CIGitLab CIForgejo Actions, écosystème GitHub Actions réutilisable
Philosophieopen-core, fonctions payantesMIT intégral, gouvernance démocratique

La machine respire, les mises à jour restent un simple changement de version dans un fichier .env (comme avant), et l’écosystème GitHub Actions est réutilisable tel quel.

Ce que j’ai perdu (assumé) : De Zot, il me manque peut-être la gestion des artefacts. Cosign UI, API des referrers, SBOM, scan de vulnérabilités : tout ça était plus riche côté Zot et je comptais m’y intéresser. Chez Forgejo, le signing est possible, mais en artefacts OCI, sans gestion first-class. “Redévelopper” les scans de sécurité est possible via les pipelines.

Ce qui reste à faire

Le décommissionnement de Zot est en cours. Il faut inventorier les images qui ne transitent pas par la CI (les copier ou les jeter), repointer les consommateurs, puis retirer le conteneur et son volume. Les derniers repos git encore sur GitLab arrivent au fil de l’eau.

Et une touche méta, pour finir : ce billet est lui-même un artefact de la migration. Le blog a été migré le 6 août, et cet article est préparé puis publié depuis le nouveau Forgejo — comme tout ce qui suivra.

Au fond, l’histoire n’est pas « j’ai changé de forge ». C’est « j’ai supprimé un composant ». Quand une machine héberge tout votre homelab, cette différence compte. Voir aussi le billet de retour du blog pour le contexte de cette refonte, ou l’article sur l’infrastructure de la plateforme data pour une autre facette de l’auto-hébergement.

Commentaires