Chaîne CI/CD (Forgejo + Woodpecker)
Le homelab héberge sa propre forge et sa propre intégration continue, en
logiciels libres, sur deux CT LXC du VLAN 10. C'est ce qui héberge les dépôts
(infra, docs…) et vérifie chaque push — y compris celui de ce portail.
| Rôle | Machine | Adresse | Logiciel |
|---|---|---|---|
| Forge Git | CT 110 forgejo (2 vCPU / 2 Go, disque 16 Go) |
https://forge.leonheu.fr (public, via Traefik/Coolify) · interne http://10.9.73.81:3000 · SSH 2222 (interne) |
Forgejo 15.0.4 |
| CI | CT 111 ci (4 vCPU / 3 Go, disque 24 Go) |
http://10.9.73.82:8000 | Woodpecker 3.16.0 |
Les deux CT sont créés par OpenTofu (opentofu/ct-forgejo-ci.tf) et configurés
par Ansible (ansible/playbooks/forgejo.yml, ci.yml). LXC Debian 12
non privilégiés ; le CT ci a l'option nesting activée pour faire tourner
Docker (backend des étapes de pipeline).
Le flux
git push ──▶ Forgejo (10.9.73.81)
│ webhook (ALLOWED_HOST_LIST = 10.9.73.82)
▼
Woodpecker server (10.9.73.82:8000)
│ gRPC :9000
▼
Woodpecker agent ──▶ étapes en conteneurs Docker
│
▼ statut du build renvoyé
Forgejo (coche verte / croix rouge sur le commit)
- Un
push(ou une PR) sur un dépôt Forgejo déclenche un webhook vers Woodpecker. Forgejo bloque par défaut les webhooks vers les IP privées : l'IP du CI est explicitement autorisée (ALLOWED_HOST_LIST = 10.9.73.82dansapp.ini). - Le server Woodpecker lit le fichier
.woodpecker/*.yamldu dépôt et planifie les étapes. - L'agent (même CT) exécute chaque étape dans un conteneur Docker
(
WOODPECKER_BACKEND=docker, 2 workflows en parallèle au plus). - Le résultat est renvoyé à Forgejo et s'affiche sur le commit / la PR.
L'authentification de Woodpecker se fait via Forgejo (OAuth2,
WOODPECKER_FORGEJO=true) : on se connecte au CI avec son compte de la forge.
L'inscription ouverte est désactivée des deux côtés
(DISABLE_REGISTRATION, WOODPECKER_OPEN=false).
Ce qui tourne dessus
infra— pipeline.woodpecker/ci.yaml:tofu fmt -check,tofu validate,ansible-lint(non bloquant pour l'instant). Letofu planen CI viendra quand la clé age sera disponible sur le runner.docs(ce portail) —mkdocs build --strict: un lien mort fait échouer le build. Le déploiement est ensuite pris en charge par Coolify (cf. Ce site).
Persistance et secrets
- Données : chacun stocke en SQLite local
(
/var/lib/forgejo/data,/var/lib/woodpecker). Ces CT sont donc sauvegardés par le vzdump quotidien comme les autres. - Secrets (mot de passe admin Forgejo, clés OAuth,
AGENT_SECRET…) : chiffrés avec sops danssecrets/services.enc.env, injectés au moment de jouer les playbooks (sops exec-env). Voir le runbook Secrets.
Miroirs externes (GitHub)
Filet de secours hors site : chaque dépôt a un push mirror Forgejo →
github.com/leonheu (dépôts privés), synchronisé à chaque push (+ toutes
les 8 h). Si le homelab disparaît, le code — y compris les secrets, chiffrés
par sops — survit.
- Config : dépôt Forgejo → Paramètres → Dépôt distant (ou API
push_mirrors). Identifiants : token GitHub fine-grained limité aux deux dépôts (permission Contents: RW), archivé dans sops (GITHUB_MIRROR_TOKEN, expire : à renouveler ~juillet 2027). - Restauration depuis le miroir :
git clone https://github.com/leonheu/<repo>puis recréer le dépôt Forgejo et pousser.
2FA et API Forgejo
Depuis l'activation de la 2FA, l'API Forgejo refuse l'authentification
par mot de passe : utiliser un jeton d'accès (Paramètres →
Applications). Le jeton d'automatisation courant est archivé dans sops
(FORGEJO_API_TOKEN, scope repository uniquement).
Exploitation
Les services sont des unités systemd (binaires, pas de conteneur pour la forge et le server/agent) :
# Sur le CT forgejo (10.9.73.81)
systemctl status forgejo
journalctl -u forgejo -f
# Sur le CT ci (10.9.73.82)
systemctl status woodpecker-server woodpecker-agent docker
journalctl -u woodpecker-agent -f
Rejouer la configuration (idempotent) sur dev-laravel — depuis le dossier
ansible/ pour qu'ansible.cfg et l'inventaire soient chargés :
cd ~/projets/infra/ansible
sops exec-env ../secrets/services.enc.env 'ansible-playbook playbooks/forgejo.yml' # ou ci.yml
Mettre à jour Forgejo ou Woodpecker
Changer forgejo_version / woodpecker_version en tête du playbook, puis
rejouer : Ansible télécharge le nouveau binaire et redémarre le service.
Faire un test de restauration du CT avant
une montée de version majeure de Forgejo (migrations de schéma SQLite).
Déploiement continu (CD) — Coolify
La CI (Woodpecker) vérifie le code ; le déploiement est assuré par
Coolify (VM 101, 10.9.73.90). C'est la dernière étape du pipeline
qui le déclenche : sur un push main, si les étapes de vérification sont
vertes, l'étape deploy appelle l'API de déploiement Coolify. Un build
rouge = pas de déploiement.
git push (main) ─▶ Woodpecker : vérifications (ex. mkdocs build --strict)
│ vert ?
▼
étape deploy ──▶ POST API Coolify ──▶ redéploiement
Brancher une nouvelle app en CD
- Créer l'app dans Coolify (build pack, branche
main, domaine, clé de déploiement ajoutée côté Forgejo). Noter sonuuid(visible dans l'URL de l'app ou son Deploy Webhook). - Token API Coolify : en créer un seul, partagé par toutes les apps
(Coolify → Keys & Tokens, permission deploy). Pas besoin d'un token par
app — c'est l'
uuidqui désigne l'app. - Secret Woodpecker : dépôt → Settings → Secrets →
coolify_token= le token, événement Push uniquement. -
Étape
deployà la fin du.woodpecker/ci.yamldu dépôt :deploy: image: curlimages/curl:8.11.1 environment: COOLIFY_TOKEN: from_secret: coolify_token commands: - 'curl -fsS -X POST -H "Authorization: Bearer $COOLIFY_TOKEN" "http://10.9.73.90:8000/api/v1/deploy?uuid=<uuid-app>&force=false"' when: - event: push branch: main -
Push sur
main→ le pipeline doit finir vert et l'app se redéployer (visible dans Deployments côté Coolify).
Piège vécu
Viser l'IP interne 10.9.73.90:8000, pas le domaine public
coolify.leonheu.fr : ce dernier résout vers l'IP publique — un
aller-retour par Internet pour deux machines du même VLAN, et un point de
défaillance de plus (DNS dynamique).
Webhook « source » sans token ?
Coolify expose aussi /webhooks/source/gitea/events/manual (validé par un
secret, sans token API). Plus élégant en théorie, mais il renvoie un
500 « Undefined variable $branch » sur la version actuelle — d'où le
choix de l'API + bearer. À retenter après une montée de version de Coolify.