Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Forgejo : maintenance courante, doctor et quotas

Par mis à jour le

Améliorer cette page

Pour entretenir une instance Forgejo 15 LTS, on pilote ses tâches planifiées (les travaux de fond qu'elle lance seule) par l'API et par des variables d'environnement, on la diagnostique avec forgejo doctor check, on mesure où grossit le disque et on plafonne les utilisateurs par des quotas. Aucune de ces opérations n'a de commande unique : les tâches se lisent dans l'API, doctor se lit à la main, les notices dans la base. La leçon s'adresse à l'administrateur dont la forge tourne déjà et qui veut savoir quoi regarder chaque semaine, et quoi faire quand une ligne passe au rouge.

Vous repartez avec cinq preuves : une tâche de fond lancée à la demande dont on a mesuré l'effet, un planning réglé par un fichier de surcharge, un défaut réparé par doctor check --fix, un quota qui refuse un push, et une notice d'échec lue dans la base. Les commandes ont été exécutées avec Forgejo 15.0.10 sur Ubuntu 24.04 (Docker 29.9.1, Compose v5.6.0, PostgreSQL 17.11), avec exactement la pile de la leçon Installer Forgejo LTS, sans écart.

  • Consulter les tâches planifiées de Forgejo par son API, et en déclencher une à la demande.
  • Régler le planning d'une tâche par un fichier de surcharge, et connaître le piège du retrait d'une variable.
  • Mesurer l'effet du git gc et du nettoyage des archives.
  • Diagnostiquer et réparer l'instance avec forgejo doctor check --run et --fix.
  • Plafonner l'espace d'un utilisateur par un quota, et comprendre quand le refus tombe.
  • Repérer où grossit le disque et retrouver l'échec d'une tâche dans les notices.

Il vous faut la forge des leçons précédentes : Forgejo 15 LTS derrière Caddy dans /opt/forgejo-lab, le nom forge.lab.test et le compte fjadmin (installation). Elle suppose aussi la variable COMPOSE_FILE du .env, qui liste les fichiers Compose de surcharge, et les variables FORGEJO__section__CLE, toutes deux expliquées dans Configuration initiale. La sauvegarde qui précède toute réparation est celle de Sauvegarder et restaurer Forgejo.

doctor check --list et --all sont déjà présentés dans Assister les utilisateurs : cette leçon va plus loin (--run, --fix, code de sortie) sans les redire. Le jeton d'accès se révoque comme dans cette leçon-là, ses portées sont celles de Sécuriser l'instance, et la montée de version est celle de Mettre à jour Forgejo.

Vous devez savoir lire un code HTTP avec curl, écrire un fichier en root avec sudo et lire du JSON. Préparez le shell de la VM : la fonction F exécute forgejo dans le conteneur sous le compte git, un jeton d'administrateur est écrit dans un fichier lisible de vous seul, et api appelle l'API avec ce jeton. Le jeton de portée all ne convient qu'à un lab jetable, et l'option -k de curl ignore le certificat de la CA du lab.

Fenêtre de terminal
cd /opt/forgejo-lab
F() { sudo docker exec -u git forgejo forgejo "$@"; }
(umask 077; F admin user generate-access-token --username fjadmin --token-name l10 --scopes all --raw > ~/.tok)
api() { local m=$1 p=$2; shift 2; curl -sk -X "$m" -H "Authorization: token $(cat ~/.tok)" -H 'Content-Type: application/json' "$@" "https://forge.lab.test/api/v1$p"; }
apic() { local m=$1 p=$2; shift 2; curl -sk -w '\nHTTP=%{http_code}\n' -X "$m" -H "Authorization: token $(cat ~/.tok)" -H 'Content-Type: application/json' "$@" "https://forge.lab.test/api/v1$p"; }
echo "jeton : $(wc -c < ~/.tok) octets"

La dernière ligne affiche jeton : 41 octets. Le fichier ~/.tok contient un secret : ne l'affichez jamais, et détruisez-le en fin de leçon (section « Comment défaire cette configuration ? »). api rend le corps de la réponse, apic ajoute la ligne HTTP=<code> : on s'en sert quand c'est le code qui compte.

Quels sont les leviers de maintenance de Forgejo ?

Section intitulée « Quels sont les leviers de maintenance de Forgejo ? »

Forgejo s'entretient par cinq leviers, chacun avec son outil : les tâches planifiées (API), forgejo doctor (CLI), les quotas (API), le disque (du et psql) et les notices (table notice). Une notice est un message que Forgejo écrit dans sa base quand une tâche de fond se termine ou échoue. Le tableau dit où chercher selon la question. Il sert de plan de la leçon : chaque ligne a sa section, et chaque section s'ouvre sur sa réponse en une phrase.

QuestionLevierSection
Que fait la forge toute seule, et comment lancer ou replanifier une tâche ?tâches planifiées, GET /api/v1/admin/cron, POST sur la tâche, variables FORGEJO__cron.…« Quelles tâches de fond Forgejo lance-t-il seul ? » puis « Comment régler le planning d'une tâche ? »
Des fichiers de l'instance sont-ils faux ?forgejo doctor check --run … --fix« Comment diagnostiquer et réparer avec doctor check ? »
Un utilisateur prend-il trop de place ?règles et groupes de quota« Comment plafonner un utilisateur par un quota ? »
Où grossit le disque ?du par dossier, psql« Où grossit la forge et comment le mesurer ? »
Une tâche a-t-elle échoué ?table notice, journal du conteneur« Comment savoir qu'une tâche planifiée a échoué ? »

Une instance saine n'a besoin d'aucune intervention : ces leviers servent à vérifier sans surprise, puis à réparer quand une vérification échoue. Pour qu'une alerte parte sans que vous lisiez ces sorties à la main, c'est la supervision qu'il faut brancher, celle de la formation Observabilité ; cette leçon donne ce qu'elle doit lire.

Forgejo planifie 30 tâches de fond, listées par GET /api/v1/admin/cron avec leur planning, leur nombre d'exécutions et leur prochaine échéance ; treize n'ont aucun planning au départ. Une tâche planifiée (on dit aussi une tâche cron, du nom du planificateur d'Unix) est un travail que Forgejo lance seul à intervalle fixe : nettoyer, vérifier, compacter. Son planning s'écrit @every 10m (toutes les dix minutes), @midnight ou @daily (une fois par jour). Il n'existe pas de sous-commande pour les voir : forgejo admin --help n'offre que user, repo-sync-releases, regenerate, auth et sendmail. Sur l'instance neuve, app.ini ne contient d'ailleurs aucune section [cron…] : tout vient des valeurs par défaut.

Fenêtre de terminal
api GET /admin/cron | python3 -c 'import sys,json
for t in json.load(sys.stdin): print(t["name"], "|", t["schedule"], "|", t["exec_times"], "|", t["next"][:19])'

Chaque ligne montre le nom, le planning, le nombre d'exécutions depuis le démarrage et la prochaine échéance. Voici un extrait de notre sortie.

update_mirrors | @every 10m | 0 | 2026-10-16T10:24:55
repo_health_check | @midnight | 0 | 2026-10-17T00:00:00
archive_cleanup | @midnight | 1 | 2026-10-17T00:00:00
delete_repo_archives | - | 0 | 0001-01-01T00:00:00
git_gc_repos | - | 0 | 0001-01-01T00:00:00
delete_old_actions | - | 0 | 0001-01-01T00:00:00
update_checker | @every 168h | 0 | 2026-10-23T10:14:55
stop_zombie_tasks | @every 5m | 1 | 2026-10-16T10:19:55
cleanup_offline_runners | - | 0 | 0001-01-01T00:00:00

Un planning - et une échéance au 0001-01-01 signalent une tâche désactivée par défaut. Parmi elles, git_gc_repos, delete_repo_archives, delete_inactive_accounts, delete_old_actions, cleanup_offline_runners et delete_old_system_notices. Les tâches actives par défaut couvrent le quotidien : nettoyage des archives (archive_cleanup), des branches supprimées, des paquets, des tâches Actions bloquées (stop_zombie_tasks), et une vérification de version toutes les 168 heures (update_checker). Le piège : tant qu'on n'active pas git_gc_repos, cette tâche ne tourne jamais, et aucun message ne le signale.

Comment lancer une tâche à la demande et mesurer son effet ?

Section intitulée « Comment lancer une tâche à la demande et mesurer son effet ? »

POST /api/v1/admin/cron/<tâche> lance la tâche immédiatement et répond 204, même si elle est désactivée ; le compteur exec_times passe alors à 1. C'est le geste pour vérifier un effet sans attendre l'échéance, ou pour purger tout de suite. La fonction cron_line affiche l'état d'une tâche avant et après.

Fenêtre de terminal
cron_line() { api GET /admin/cron | python3 -c 'import sys,json
for t in json.load(sys.stdin):
if t["name"]=="'"$1"'": print(t["name"],"| schedule:",t["schedule"],"| exec_times:",t["exec_times"],"| prev:",t["prev"][:19])'; }
cron_line git_gc_repos
apic POST /admin/cron/git_gc_repos | tail -2
sleep 8
cron_line git_gc_repos

Avant : git_gc_repos | schedule: - | exec_times: 0 | prev: 0001-01-01T00:00:00. La requête répond HTTP=204. Après huit secondes : exec_times: 1 | prev: 2026-10-16T10:16:15. La tâche désactivée s'est exécutée une fois, sans rien changer à son planning. Le code 204 dit que la demande est acceptée, pas que la tâche a réussi : l'effet se mesure sur le disque, et l'échec se lit dans les notices (section « Comment savoir qu'une tâche planifiée a échoué ? »).

git_gc_repos compacte les objets de chaque dépôt : sur un dépôt de 40 pushes, les 123 objets libres sont passés dans un seul pack et le dossier est passé de 1872 à 1024 Kio. Git range chaque version d'un fichier dans un objet, d'abord isolé (« libre »). git gc (garbage collection, le ménage de Git) regroupe les objets libres dans un seul fichier compressé, un pack, et supprime ce qui ne sert plus. Pour le montrer, il faut un dépôt qui a de l'historique. Le bloc crée gc-demo2, clone avec la CA de Caddy et pousse 40 fichiers de 20 Ko. L'en-tête d'authentification porte le jeton, d'où credential.helper= vide et GIT_TERMINAL_PROMPT=0.

Fenêtre de terminal
W=$(mktemp -d); cd "$W"
sudo docker cp forgejo-proxy:/data/caddy/pki/authorities/local/root.crt "$W/root.crt"; sudo chown "$USER" "$W/root.crt"
export GIT_SSL_CAINFO="$W/root.crt" GIT_TERMINAL_PROMPT=0
H=(-c credential.helper= -c "http.extraHeader=Authorization: token $(cat ~/.tok)")
CO() { sudo docker exec -u git forgejo git -C /data/git/repositories/fjadmin/gc-demo2.git count-objects -v | tr '\n' ' '; echo; }
apic POST /user/repos -d '{"name":"gc-demo2","auto_init":true,"default_branch":"main"}' | tail -1
git "${H[@]}" clone -q https://forge.lab.test/fjadmin/gc-demo2.git r && cd r
git config user.email a@example.test; git config user.name a
for i in $(seq 1 40); do head -c 20000 /dev/urandom | base64 > "f$i.txt"; git add .; git commit -qm "c$i"; git "${H[@]}" push -q origin main; done
echo "AVANT :"; CO
sudo docker exec forgejo du -sk /data/git/repositories/fjadmin/gc-demo2.git
apic POST /admin/cron/git_gc_repos | tail -1
sleep 8
echo "APRES :"; CO
sudo docker exec forgejo du -sk /data/git/repositories/fjadmin/gc-demo2.git
cd "$W"

La création répond HTTP=201. La mesure avant est count: 123 size: 1292 in-pack: 0 packs: 0 size-pack: 0 et 1872 Kio ; la mesure après est count: 0 size: 0 in-pack: 123 packs: 1 size-pack: 815 et 1024 Kio. Les objets libres (count) sont tous passés dans un pack (in-pack).

Le piège est dans CO. Lancé en root (sans -u git), git count-objects échoue avec fatal: detected dubious ownership in repository, parce que le dépôt appartient à git : c'est un garde-fou de Git, pas une panne. Nous avons obtenu cette erreur à notre premier essai. Seul du répondait, d'où les deux mesures.

Les archives (les .zip générés quand on télécharge une branche) obéissent à deux tâches. Sur un dépôt gc-demo, créé comme gc-demo2 (sans les 40 pushes), nous avons téléchargé main.zip puis lancé delete_repo_archives. Sans ce dépôt, le téléchargement répond 404.

Fenêtre de terminal
apic POST /user/repos -d '{"name":"gc-demo","auto_init":true,"default_branch":"main"}' | tail -1
curl -sk -o /dev/null -w 'archive http=%{http_code}\n' -H "Authorization: token $(cat ~/.tok)" https://forge.lab.test/api/v1/repos/fjadmin/gc-demo/archive/main.zip
sudo docker exec forgejo sh -c 'find /data/gitea/repo-archive -type f; du -sk /data/gitea/repo-archive'
apic POST /admin/cron/delete_repo_archives | tail -2
sleep 4
sudo docker exec forgejo sh -c 'find /data/gitea/repo-archive -type f; du -sk /data/gitea/repo-archive'

La création répond HTTP=201, le téléchargement archive http=200, la première mesure montre un .zip et 28 Kio, la seconde plus aucun fichier et 24 Kio (les sous-dossiers vides restent). Cette tâche supprime toutes les archives ; archive_cleanup, planifiée à minuit, est celle du quotidien : la documentation lui donne OLDER_THAN = 24h et elle ne supprime donc que les archives plus vieilles qu'un jour. Nous ne l'avons pas lancée. Les autres tâches de nettoyage (repo_health_check, cleanup_actions, delete_old_actions, delete_inactive_accounts, delete_old_system_notices) ont répondu 204 et incrémenté exec_times, sans effet mesurable : l'instance était vide, il n'y avait rien à nettoyer.

On pose des variables FORGEJO__cron.<tâche>__ENABLED, __SCHEDULE, __NOTICE_ON_SUCCESS ou __OLDER_THAN dans un fichier Compose de surcharge ajouté à COMPOSE_FILE, puis on recrée le conteneur. Le point dans le nom de la section (cron.git_gc_repos) fonctionne tel quel. Le fichier suivant active le gc toutes les deux minutes (un rythme de démonstration, pas de production), demande une notice à chaque succès, ramène le nettoyage des archives à toutes les 30 minutes (au lieu de minuit) et active la suppression des comptes jamais activés.

Fenêtre de terminal
sudo tee compose.maintenance.yaml >/dev/null <<'YAML'
services:
server:
environment:
FORGEJO__cron.git_gc_repos__ENABLED: "true"
FORGEJO__cron.git_gc_repos__SCHEDULE: "@every 2m"
FORGEJO__cron.git_gc_repos__NOTICE_ON_SUCCESS: "true"
FORGEJO__cron.archive_cleanup__SCHEDULE: "@every 30m"
FORGEJO__cron.delete_inactive_accounts__ENABLED: "true"
FORGEJO__cron.delete_inactive_accounts__SCHEDULE: "@daily"
FORGEJO__cron.delete_inactive_accounts__OLDER_THAN: "168h"
FORGEJO__quota__ENABLED: "true"
YAML
sudo grep -q '^COMPOSE_FILE=' .env || echo 'COMPOSE_FILE=compose.yaml:compose.maintenance.yaml' | sudo tee -a .env >/dev/null
sudo docker compose up -d 2>&1 | tail -4

La dernière variable, FORGEJO__quota__ENABLED, sert à la section sur les quotas. Sur notre installation neuve, COMPOSE_FILE n'existait pas : la deuxième commande l'a créée, et docker compose config --services a listé db, server et proxy. Sur une pile qui porte déjà COMPOSE_FILE, on ajoute le fichier à la liste comme dans la leçon de sécurité, avec sudo sed -i 's#^COMPOSE_FILE=.*#&:compose.maintenance.yaml#' .env, ce que nous n'avons pas rejoué sur cette installation. Le .env appartient à root en mode 600 : c'est pourquoi le test se fait avec sudo grep, sans quoi grep échoue sur « Permission denied » et la ligne serait ajoutée en double. Lancez toujours docker compose depuis /opt/forgejo-lab, où se trouve le .env.

Vérifiez d'abord ce que Forgejo a écrit dans app.ini, puis attendez un tick du planning.

Fenêtre de terminal
sudo docker exec forgejo sh -c 'sed -n "/^\[cron\./,/^$/p;/^\[quota\]/,/^$/p" /data/gitea/conf/app.ini'
sleep 150
cron_line git_gc_repos

Le fichier contient les sections [cron.git_gc_repos] (ENABLED = true, NOTICE_ON_SUCCESS = true, SCHEDULE = @every 2m), [cron.delete_inactive_accounts] (OLDER_THAN = 168h), [cron.archive_cleanup] et [quota] (ENABLED = true). Après 150 secondes, git_gc_repos affiche schedule: @every 2m | exec_times: 1 avec une prochaine échéance : la tâche s'est lancée seule, sans POST. Le fichier de surcharge est donc bien pris en compte, sans aucune édition à la main d'app.ini.

Que devient une clé quand on retire sa variable ?

Section intitulée « Que devient une clé quand on retire sa variable ? »

Retirer la variable du fichier de surcharge ne retire pas la clé de app.ini : Forgejo garde la dernière valeur écrite. C'est le piège classique des variables FORGEJO__, déjà vu dans la leçon de sécurité. Nous l'avons éprouvé en supprimant la ligne archive_cleanup__SCHEDULE du fichier, en recréant le conteneur, puis en lisant app.ini.

Fenêtre de terminal
grep -c 'archive_cleanup__SCHEDULE' compose.maintenance.yaml
sudo sed -i '/archive_cleanup__SCHEDULE/d' compose.maintenance.yaml
grep -c 'archive_cleanup__SCHEDULE' compose.maintenance.yaml
sudo docker compose up -d 2>&1 | grep -E "Recreate|Started" | head -3
sudo docker exec forgejo env | grep -c 'archive_cleanup__SCHEDULE'
sudo docker exec forgejo sh -c 'grep -A1 "^\[cron.archive_cleanup\]" /data/gitea/conf/app.ini'

Le premier compteur vaut 1 (la ligne est encore là), les trois suivants valent 0 : la variable n'est plus ni dans le fichier ni dans le conteneur recréé (Container forgejo Recreate). Pourtant app.ini affiche toujours [cron.archive_cleanup] et SCHEDULE = @every 30m. Pour revenir à la valeur de départ, il faut donc la fixer explicitement : le défaut relevé plus haut dans la liste est @midnight, mais nous n'avons pas rejoué cette correction. Un premier essai, lancé depuis un dossier temporaire, n'avait recréé aucun conteneur : la commande se lance depuis le dossier du projet.

Comment diagnostiquer et réparer avec doctor check ?

Section intitulée « Comment diagnostiquer et réparer avec doctor check ? »

forgejo doctor check lance des vérifications nommées ; --run <nom> en choisit une, --fix répare ce qui peut l'être, et le code de sortie vaut 0 même quand la vérification affiche ERROR. Le dernier point est le piège de la leçon : une supervision ne peut pas se fier au code. La liste des vérifications (--list) est celle de la leçon sur les comptes ; on la relit ici pour choisir un nom, puis on lit les options avec --help.

Fenêtre de terminal
F doctor check --list
F doctor check --help | sed -n '1,40p'

Les 28 vérifications de la liste comptent 5 par défaut (marquées d'une étoile) : paths, check-db-version, check-user-type, authorized-keys et synchronize-repo-heads. Les options utiles ici sont --run (une vérification par son nom, répétable), --default, --all, --fix (« Automatically fix what we can »), --log-file (- envoie le journal détaillé sur la sortie standard) et --color. Le nom doit être exact : F doctor check --run check-hooks répond Command error: unknown checks: "check-hooks" avec le code 1. La vérification des hooks s'appelle hooks.

Le fichier authorized_keys de l'utilisateur git est reconstruit à partir de la base : si on le vide, doctor affiche [E] puis ERROR, et --fix le régénère. Ce fichier dit à OpenSSH quelles clés publiques ont le droit de se connecter au compte git, donc de pousser par SSH. Pour le montrer, ajoutons une clé SSH à fjadmin par l'API (la base la connaît, donc le fichier doit la contenir), vérifions que tout va bien, puis vidons le fichier.

Fenêtre de terminal
K=$(mktemp -d); ssh-keygen -q -t ed25519 -N '' -f "$K/k"
apic POST /user/keys -d "{\"title\":\"doc\",\"key\":\"$(cat $K/k.pub)\"}" | tail -1
sudo docker exec forgejo sh -c 'grep -c "" /data/git/.ssh/authorized_keys'
F doctor check --run authorized-keys; echo "rc=$?"
sudo docker exec forgejo sh -c ': > /data/git/.ssh/authorized_keys'
F doctor check --run authorized-keys 2>&1; echo "rc=$?"

L'ajout répond HTTP=201 et le fichier compte 2 lignes. Le premier passage affiche OK et rc=0. Après le vidage, la sortie devient :

[1] Check if OpenSSH authorized_keys file is up-to-date
- [I] AppPath set to '/usr/local/bin/gitea' (was '/usr/local/bin/forgejo')
- [E] Key in database is not present in /data/git/.ssh/authorized_keys: command="/usr/local/bin/gitea --config=/data/gitea/conf/app.ini serv key-2",…
- [E] authorized_keys file "/data/git/.ssh/authorized_keys" contains validity errors.
Regenerate it with:
"forgejo admin regenerate keys"
or
"forgejo doctor check --run authorized-keys --fix"
ERROR
All done (checks: 1).
rc=0

Lisez-la dans l'ordre : la ligne [E] donne le défaut (une clé de la base absente du fichier), la ligne ERROR conclut, et la sortie propose deux réparations. Le rc=0 est le piège : malgré ERROR, le code de sortie n'est pas 1. Une supervision doit donc lire le texte (la présence de [E] ou de ERROR), pas le code. La ligne [I] AppPath set to '/usr/local/bin/gitea' est normale : elle apparaît même quand tout va bien et ne signale pas une erreur de configuration.

Fenêtre de terminal
F doctor check --run authorized-keys --fix; echo "rc=$?"
sudo docker exec forgejo sh -c 'grep -c "" /data/git/.ssh/authorized_keys'
F doctor check --run authorized-keys; echo "rc=$?"
rm -rf "$K"

Le passage avec --fix affiche OK, le fichier compte de nouveau 2 lignes, et le contrôle suivant affiche OK. La sauvegarde de la leçon précédente reste le garde-fou : --fix modifie la base ou le stockage, et il vaut mieux savoir revenir en arrière avant de le lancer sur une instance réelle.

Chaque dépôt porte des scripts (hooks) par lesquels Forgejo contrôle les pushes ; doctor check --run hooks les compare à ce qu'il génère, et --fix les régénère. Le hook pre-receive s'exécute sur le serveur avant d'accepter un push : c'est lui qui, plus bas, refuse un push hors quota. Nous l'avons supprimé du dépôt gc-demo (celui de la section sur le gc), vérifié sa disparition (ls … | grep -c '^pre-receive$' rend 0) et lancé la vérification.

Fenêtre de terminal
R=/data/git/repositories/fjadmin/gc-demo.git
sudo docker exec forgejo sh -c "rm -f $R/hooks/pre-receive; ls $R/hooks | grep -c '^pre-receive$'"
F doctor check --run hooks 2>&1; echo "rc=$?"
F doctor check --run hooks --fix 2>&1; echo "rc=$?"
sudo docker exec forgejo sh -c "ls -l $R/hooks/pre-receive"
F doctor check --run hooks 2>&1; echo "rc=$?"

Le premier passage affiche des lignes [W] (avertissement), notamment old hook file …/hooks/pre-receive does not exist, puis OK et rc=0. Les avertissements ne font donc pas changer le verdict : là encore, il faut lire les lignes. Avec --fix, la sortie ajoute [W] Regenerated hooks for fjadmin/gc-demo, le fichier pre-receive est de retour (-rwxr-xr-x, 376 octets), et le dernier contrôle n'affiche plus que OK.

Une réserve. Sur un dépôt intact et récent, hooks affiche déjà quatre lignes [W] (pre-receive.d/gitea, update.d/gitea, post-receive.d/gitea et proc-receive « is out of date »), puis OK et rc=0. Le passage avec --fix les régénère pour tous les dépôts de l'instance (ici gc-demo2 aussi), et le contrôle suivant n'affiche plus que OK. La cause de ces lignes n'a pas été trouvée ; retenez que hooks peut signaler des fichiers périmés sur un dépôt récent, et que --fix les régénère.

Un quota se compose d'une règle (une limite en octets sur un sujet comme size:repos:all), d'un groupe qui porte la règle, et d'une affectation de l'utilisateur au groupe ; la fonctionnalité s'allume avec [quota] ENABLED. Un quota plafonne l'espace qu'un utilisateur peut occuper, ici avec ses dépôts. Le fichier de la section précédente a déjà posé FORGEJO__quota__ENABLED. Au départ, GET /admin/quota/groups et GET /admin/quota/rules répondent [] : aucune règle n'existe. Créons un utilisateur dev1, dont le mot de passe est tiré au hasard et jamais affiché, et son jeton.

Fenêtre de terminal
PW=$(openssl rand -hex 12)
F admin user create --username dev1 --email dev1@example.test --password "$PW" --must-change-password=false
T1=$(F admin user generate-access-token --username dev1 --token-name q --scopes all --raw)
u1() { local m=$1 p=$2; shift 2; curl -sk -w '\nHTTP=%{http_code}\n' -X "$m" -H "Authorization: token $T1" -H 'Content-Type: application/json' "$@" "https://forge.lab.test/api/v1$p"; }
apic POST /admin/quota/rules -d '{"name":"depots-1mio","limit":1048576,"subjects":["size:repos:all"]}'
apic POST /admin/quota/groups -d '{"name":"petits-depots"}'
apic PUT /admin/quota/groups/petits-depots/rules/depots-1mio | tail -1
apic PUT /admin/quota/groups/petits-depots/users/dev1 | tail -1
apic GET /admin/quota/groups | cut -c1-700

La limite est en octets : 1048576, soit 1 Mio. Les deux créations répondent HTTP=201, les deux affectations HTTP=204, et la dernière commande montre le groupe petits-depots portant la règle depots-1mio ("subjects":["size:repos:all"]). La consommation de l'utilisateur se lit en son nom avec u1 GET /user/quota, ou par l'administrateur avec apic GET /admin/users/dev1/quota, qui ajoute le nom du groupe. Au départ, tous les compteurs valent 0.

Le contrôle porte sur l'usage déjà connu au moment du push : le push qui fait dépasser la limite passe, et c'est le suivant qui est refusé. La documentation parle d'un quota souple (« soft »), contrôlé avant l'action, encore en développement. C'est le point à retenir avant de choisir une limite : un quota de 1 Mio ne garantit pas qu'un dépôt reste sous 1 Mio, il garantit qu'on ne pousse plus une fois la limite franchie. Le schéma suit les trois pushes de dev1 de haut en bas : regardez ce que le hook compare à chaque fois : l'usage déjà connu, mis à jour quelques secondes après un push, et jamais la taille du push en cours. La ligne qui change entre le deuxième push (accepté) et le troisième (refusé) est cet usage connu.

Trois pushes de l'utilisateur dev1 sous une règle de quota de 1048576 octets, en séquence : le push 1 de 400 Ko est accepté car l'usage connu, environ 24 Ko, est sous la limite ; le push 2 de 1,5 Mo est accepté car l'usage connu est encore sous la limite (la taille du push n'est pas ajoutée), et l'usage connu passe, quelques secondes après, à environ 1,9 Mo ; le push 3 de quelques octets est refusé avec Quota exceeded car l'usage connu dépasse la limite ; la sortie du groupe de quota rétablit le push.Usage connu(limite 1 Mio)Hook pre-receivede ForgejoUtilisateur dev1(git push)sortie du groupe de quota : le push 3 passepush 1, 400 Kousage connu environ24 Ko, sous la limite ?ouiacceptépush 2, 1,5 Mousage connu sous lalimite ?oui (la taille du pushn'est pas ajoutée)accepté, quelques secondes plus tardl'usage connu passe à environ 1,9 Mopush 3, quelques octetsusage connu environ1,9 Mo, sous la limite?nonrefusé, Quota exceeded
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Voici les gestes. dev1 crée un dépôt, clone, puis pousse un fichier de 400 Ko, lit sa consommation, pousse un fichier de 1,5 Mo, relit sa consommation et tente de créer un second dépôt.

Fenêtre de terminal
u1 POST /user/repos -d '{"name":"q1","auto_init":true,"default_branch":"main"}' | tail -1
H=(-c credential.helper= -c "http.extraHeader=Authorization: token $T1")
git "${H[@]}" clone -q https://forge.lab.test/dev1/q1.git q && cd q
git config user.email d@example.test; git config user.name d
head -c 400000 /dev/urandom > a.bin; git add .; git commit -qm a
git "${H[@]}" push origin main 2>&1 | tail -3; echo "rc=${PIPESTATUS[0]}"
sleep 5; u1 GET /user/quota
head -c 1500000 /dev/urandom > b.bin; git add .; git commit -qm b
git "${H[@]}" push origin main 2>&1 | tail -6; echo "rc=${PIPESTATUS[0]}"
sleep 5; u1 GET /user/quota
u1 POST /user/repos -d '{"name":"q2"}' | cut -c1-300

Avant le premier push, la consommation vaut "repos":{"public":23696,…} (le dépôt vide avec son README). Le premier push est accepté (rc=0), et la consommation lue 5 secondes après est 424318. Le compteur est mis à jour de façon asynchrone : lu juste après le push, il affiche encore l'ancien chiffre (nous l'avons vu passer de 23696 à 424318 en moins de 2 secondes), d'où les sleep 5. Le second push, de 1,5 Mo, est accepté aussi (rc=0), alors que la limite est de 1 048 576 octets, puisque l'usage connu (424318) est encore sous la limite : la consommation passe à 1925303 octets (le chiffre varie de quelques octets, les fichiers étant aléatoires). La création d'un dépôt est ensuite refusée par HTTP=413 et le message {"message":"quota exceeded","user_id":2,"username":"dev1"}.

Fenêtre de terminal
cd ..
git "${H[@]}" clone -q https://forge.lab.test/dev1/q1.git q3 && cd q3
git config user.email d@example.test; git config user.name d
echo petit > c.txt; git add .; git commit -qm c
git "${H[@]}" push origin main 2>&1 | tail -6; echo "rc=${PIPESTATUS[0]}"
u1 POST /repos/dev1/q1/issues -d '{"title":"t"}' | cut -c1-200

Le cd .. sort du premier clone : sans lui, git clone créerait un clone imbriqué sans identité Git, et le commit échouerait. Le clone fonctionne malgré le dépassement (3912 Kio avec l'arbre de travail), et la création d'un ticket répond HTTP=201. Le troisième push, de quelques octets, est refusé :

remote:
remote: Forgejo: Quota exceeded
remote:
To https://forge.lab.test/dev1/q1.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'https://forge.lab.test/dev1/q1.git'
rc=1

Le message Quota exceeded vient du hook pre-receive : le serveur a refusé le push, l'utilisateur voit pre-receive hook declined. Le clone et les tickets continuent de marcher ; le push et la création de dépôt sont refusés. La sortie de l'utilisateur du groupe rétablit l'écriture.

Fenêtre de terminal
apic DELETE /admin/quota/groups/petits-depots/users/dev1 | tail -1
git "${H[@]}" push origin main 2>&1 | tail -3; echo "rc=${PIPESTATUS[0]}"

La suppression répond HTTP=204 et le push repasse (b4f976e..4bc94fc main -> main, rc=0 ; les empreintes varient). Pour en tirer une règle d'usage : une limite doit être choisie en tenant compte du plus gros push attendu, car elle ne l'interrompt pas.

L'essentiel de l'espace d'une forge se trouve dans les dépôts (/data/git/repositories) et dans la base PostgreSQL ; les autres dossiers du volume /data se mesurent un par un avec du -sk. Mesurer d'abord, c'est savoir où regarder le jour où le disque se remplit. Le bloc passe en revue les dossiers de /data que Forgejo utilise, dans le conteneur.

Fenêtre de terminal
sudo docker exec forgejo sh -c 'cd /data; for d in git/repositories gitea/attachments gitea/lfs gitea/repo-archive gitea/actions_log gitea/actions_artifacts gitea/packages gitea/avatars gitea/repo-avatars gitea/indexers gitea/queues gitea/sessions gitea/log gitea/conf git/.ssh; do [ -e $d ] && du -sk $d || echo "(absent) $d"; done; echo; ls gitea'

Voici nos mesures, en Kio, sur une instance de test à ce moment-là.

DossierTaille (Kio)
git/repositories3508
gitea/indexers96
gitea/queues40
gitea/repo-archive24
gitea/avatars16
gitea/attachments, actions_log, actions_artifacts, packages, repo-avatars, log4 chacun
gitea/lfs, gitea/sessionsabsents

Le dossier git/repositories domine, et gitea/lfs n'existe pas encore : Forgejo crée ses dossiers à l'usage. Les dossiers à surveiller sont donc ceux des fonctions que vous activez (pièces jointes, paquets, artefacts et journaux des Actions). La commande F doctor check --run paths affiche en complément les chemins de la configuration, dont Repository Root Path (/data/git/repositories) et Data Root Path (/data/gitea).

La base se mesure avec psql dans le conteneur forgejo-db, et les volumes Docker de l'hôte avec du.

Fenêtre de terminal
sudo docker exec forgejo-db psql -U forgejo -d forgejo -c "select pg_size_pretty(pg_database_size('forgejo')) as base"
sudo docker exec forgejo-db psql -U forgejo -d forgejo -c "select relname, pg_size_pretty(pg_total_relation_size(c.oid)) as taille from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname='public' and c.relkind='r' order by pg_total_relation_size(c.oid) desc limit 6"
sudo du -sk /var/lib/docker/volumes/forgejo-lab_forgejo-data/_data /var/lib/docker/volumes/forgejo-lab_postgres-data/_data

La base pèse 16 MB, et les plus grosses tables sont repository (304 kB), issue (208 kB), action (184 kB), access_token (144 kB), user (128 kB) et action_task (112 kB). Côté hôte, le volume forgejo-data pèse 3804 Kio et le volume postgres-data 72524 Kio : le dossier du volume est plus gros que la base mesurée par pg_database_size, et nous n'avons pas cherché pourquoi. Dimensionnez le disque d'après le volume, pas d'après la seule base.

Comment savoir qu'une tâche planifiée a échoué ?

Section intitulée « Comment savoir qu'une tâche planifiée a échoué ? »

Une tâche en échec écrit une notice dans la table notice de la base et une ligne [W] ou [E] dans le journal du conteneur ; aucune API ne les expose. GET /api/v1/admin/notices répond 404 page not found. Avec NOTICE_ON_SUCCESS, une tâche écrit aussi une notice à chaque succès, ce qui permet de vérifier qu'elle tourne. Pour provoquer un échec, nous avons créé un dépôt gc-broken puis renommé son dossier objects, ce qui le rend illisible pour Git.

Fenêtre de terminal
apic POST /user/repos -d '{"name":"gc-broken","auto_init":true,"default_branch":"main"}' | tail -1
B=/data/git/repositories/fjadmin/gc-broken.git
sudo docker exec forgejo sh -c "mv $B/objects $B/objects.bak"
apic POST /admin/cron/repo_health_check | tail -1
apic POST /admin/cron/git_gc_repos | tail -1
sleep 10
sudo docker exec forgejo-db psql -U forgejo -d forgejo -c "select id, type, left(replace(description, E'\n', ' '), 230) as description from notice order by id"
sudo docker logs forgejo --since 1m 2>&1 | grep -iE "\[E\]|\[W\]|health|gc-broken" | cut -c1-250 | head -8
sudo docker exec forgejo sh -c "mv $B/objects.bak $B/objects"

Les deux déclenchements répondent HTTP=204, ce qui confirme que 204 ne dit pas que la tâche a réussi. La table contient alors cinq lignes dans notre rejeu (le nombre dépend des passages du planning toutes les deux minutes). Trois sont de type 2 : Cron: Garbage collect all repositories has finished (le succès demandé par NOTICE_ON_SUCCESS). Deux sont de type 1, les échecs : Failed to health check repository (fjadmin/gc-broken): exit status 128 et Repository garbage collection failed for /data/git/repositories/fjadmin/gc-broken.git. Stdout: Error: exit status 128 - fatal: not a git repository….

Le journal du conteneur dit la même chose, avec le niveau :

.../repository/check.go:52:GitFsckRepo() [W] Failed to health check repository (<Repository 2:fjadmin/gc-broken>): exit status 128
.../repository/check.go:94:GitGcRepo() [E] Repository garbage collection failed for <Repository 2:fjadmin/gc-broken>. Stdout:

La supervision d'une forge peut donc lire ces deux sources : la table notice (requête SQL) et les lignes [E] du journal. Dans nos essais, le type 1 accompagne les échecs et le type 2 les succès ; nous n'avons pas lu les notices dans l'interface web d'administration. Le dossier objects est remis en place par la dernière commande.

Que faire quand la maintenance ne se passe pas comme prévu ?

Section intitulée « Que faire quand la maintenance ne se passe pas comme prévu ? »

Les échecs de cette leçon se lisent dans la sortie de doctor, dans les notices, dans le message du hook ou dans l'état de app.ini. Le tableau relie chaque symptôme à sa cause.

SymptômeCauseCorrection
fatal: detected dubious ownership in repository avec git count-objectsla commande tourne en root alors que le dépôt appartient à gitdocker exec -u git forgejo git -C <dépôt> count-objects -v
Command error: unknown checks: "check-hooks" (code 1)le nom de la vérification n'existe pasforgejo doctor check --list, le nom est hooks
doctor affiche [E] et ERROR, code de sortie 0le code ne reflète pas le verdictlire le texte, relancer avec --fix
[I] AppPath set to '/usr/local/bin/gitea'information normale de authorized-keysrien à faire
hooks signale is out of date sur un dépôt récentcause non trouvée--run hooks --fix
Une tâche n'a jamais d'échéance (-, 0001-01-01)désactivée par défautENABLED et SCHEDULE dans un fichier de surcharge
POST /admin/cron/<tâche> répond 204 mais rien ne changela demande est acceptée, pas validéelire exec_times, la notice et le journal
La valeur d'une tâche reste après retrait de la variableapp.ini garde la cléfixer explicitement la valeur voulue
remote: Forgejo: Quota exceeded et pre-receive hook declinedl'usage connu dépasse la limitesortir l'utilisateur du groupe
HTTP 413 quota exceeded à la création d'un dépôtmême causemême correction
GET /api/v1/admin/notices répond 404cette API n'existe pas en 15.0.10lire la table notice

Le diagnostic commence par une question : la vérification est-elle disponible sous ce nom, et quelle ligne est en [E] ? Ensuite, selon la réponse, soit --fix (fichiers de l'instance), soit une action sur la tâche (planning, POST), soit une action sur l'utilisateur (quota).

On supprime le groupe puis la règle de quota, les dépôts et l'utilisateur de test, la clé SSH et les notices, on remet authorized_keys d'aplomb, puis on détruit le jeton. Chaque suppression par l'API répond 204.

Fenêtre de terminal
apic DELETE /admin/quota/groups/petits-depots | tail -1
apic DELETE /admin/quota/rules/depots-1mio | tail -1
for r in fjadmin/gc-demo fjadmin/gc-demo2 fjadmin/gc-broken dev1/q1; do echo -n "$r "; curl -sk -o /dev/null -w '%{http_code}\n' -X DELETE -H "Authorization: token $(cat ~/.tok)" https://forge.lab.test/api/v1/repos/$r; done
echo -n "dev1 "; curl -sk -o /dev/null -w '%{http_code}\n' -X DELETE -H "Authorization: token $(cat ~/.tok)" "https://forge.lab.test/api/v1/admin/users/dev1?purge=true"
KID=$(api GET /user/keys | python3 -c 'import sys,json; print(" ".join(str(k["id"]) for k in json.load(sys.stdin)))')
for k in $KID; do curl -sk -o /dev/null -w "cle $k %{http_code}\n" -X DELETE -H "Authorization: token $(cat ~/.tok)" https://forge.lab.test/api/v1/user/keys/$k; done
sudo docker exec forgejo-db psql -U forgejo -d forgejo -Atc "delete from notice"
F doctor check --run authorized-keys --fix | tail -3
shred -u ~/.tok
sudo docker exec forgejo-db psql -U forgejo -d forgejo -Atc "delete from access_token; select count(*) from access_token"

Les dépôts, l'utilisateur et la clé répondent 204, DELETE 5 (le nombre varie) retire les notices, et la dernière commande affiche DELETE 1 puis 0. Attention : ce dernier DELETE supprime tous les jetons de l'instance, ce qui ne convient qu'à un lab jetable ; sur une forge réelle, révoquez le seul jeton l10 comme dans la leçon Assister les utilisateurs. Le groupe et la règle vides ramènent GET /admin/quota/groups à [].

Le fichier compose.maintenance.yaml et la ligne COMPOSE_FILE ne sont pas retirés par ce bloc. Retirer le fichier de COMPOSE_FILE ne vide pas app.ini (voir « Que devient une clé quand on retire sa variable ? ») : les sections [cron.git_gc_repos], [cron.delete_inactive_accounts], [cron.archive_cleanup] et [quota] y resteraient. Sur une pile qui n'avait que ce fichier dans COMPOSE_FILE, nous avons retiré la ligne et le fichier, supprimé ces sections en tant que git (jamais en root), puis redémarré le conteneur.

Fenêtre de terminal
sudo sed -i '/^COMPOSE_FILE=/d' .env
sudo rm -f compose.maintenance.yaml
sudo docker compose up -d
sudo docker exec -u git forgejo sh -c "sed -i '/^\[cron\./,/^\$/d;/^\[quota\]/,/^\$/d' /data/gitea/conf/app.ini"
sudo docker compose restart server

Le contrôle grep -cE '^\[(cron|quota)' /data/gitea/conf/app.ini, lancé dans le conteneur, rend alors 0. Si votre COMPOSE_FILE porte d'autres fichiers, retirez seulement :compose.maintenance.yaml de la liste.

  • Forgejo planifie 30 tâches de fond, visibles par GET /api/v1/admin/cron et non par la CLI ; treize n'ont pas de planning par défaut, dont git_gc_repos, delete_repo_archives et delete_old_actions.
  • POST /api/v1/admin/cron/<tâche> répond 204 même pour une tâche désactivée et la lance : le 204 ne prouve pas le succès, l'effet se mesure sur le disque et l'échec se lit dans les notices.
  • git_gc_repos a fait passer un dépôt de 40 pushes de 1872 à 1024 Kio ; delete_repo_archives supprime toutes les archives (le fichier .zip disparaît, le dossier passe de 28 à 24 Kio). Sur une instance vide, les autres tâches n'ont eu aucun effet mesurable.
  • git count-objects en root échoue sur dubious ownership : passer par docker exec -u git.
  • Le planning se règle par FORGEJO__cron.<tâche>__ENABLED, __SCHEDULE, __NOTICE_ON_SUCCESS, __OLDER_THAN dans un fichier de surcharge de COMPOSE_FILE ; retirer la variable ne retire pas la clé d'app.ini, il faut fixer la valeur voulue.
  • forgejo doctor check --run <nom> --fix répare ce qui peut l'être ; le nom doit être exact (hooks, pas check-hooks), et le code de sortie vaut 0 même avec ERROR : une supervision lit le texte.
  • Un quota se compose d'une règle (limit en octets, subjects size:repos:all), d'un groupe et d'une affectation ; le push qui fait dépasser passe, le suivant est refusé (Quota exceeded), la création de dépôt répond 413, le clone et les tickets restent possibles.
  • Le disque se mesure avec du -sk par dossier de /data et psql pour la base ; les dépôts dominent sur une instance neuve.
  • Il n'existe pas d'API des notices ; la table notice et le journal du conteneur ([W], [E]) portent les échecs.

Six questions tirées sur huit : où lire les tâches planifiées, ce que prouve un lancement à la demande, la clé qui reste dans app.ini, le code de sortie de doctor, le quota et le nom exact d'une vérification.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn