GitOps — Forgejo + Renovate + CD¶
Date : 2026-05-07
Statut : ✅ Terminé — stabilisé après correctifs post-déploiement
Objectif¶
Mettre en place un système de mise à jour automatisé à l'échelle du serveur basé sur Git comme source de vérité :
- Renovate détecte les nouvelles versions d'images Docker (toutes les 6h)
- Forgejo héberge les compose files versionnés + crée des PRs automatiquement
- Forgejo Actions déploie via SSH sur Proxmox à chaque merge sur
main - Zot registry sert de proxy pull-through (Docker Hub, GHCR, lscr.io) pour tous les LXC, et résout l'absence d'internet sur LXC 103
Décisions arrêtées¶
| Décision | Choix | Justification |
|---|---|---|
| Emplacement Forgejo | Nouveau LXC 107 "forge" | Isolation CI/CD, usage général futur |
| Registry scope | Zot sur LXC 107, tous LXC | Cache global + LXC 103 sans internet |
| CD engine | SSH Proxmox → pct exec | Volumes relatifs incompatibles avec Portainer Git-backed |
| Majors | Review manuelle | Breaking changes historiques sur *arr stack |
| Minor/patch | Auto-merge après 3 jours | stabilityDays: 3 Renovate |
| Secrets Git | Exclus (.gitignore), stack.env | Gitignored + backup séparé |
| LXC 103 / GHCR | Image ref explicite Zot mirror | Une seule image hors Docker Hub : Cup |
Architecture déployée¶
Renovate (cron 06h00)
→ scanne homelab/infra sur git.ncls.ltd
→ PR avec bump version (+ changelog GitHub)
Review (manuelle pour majors, 3j stabilité pour minor/patch)
→ merge → push main
Forgejo Actions (deploy.yml)
→ SSH root@192.168.1.21 (Proxmox host)
→ git pull /mnt/lxc-data
→ pct exec NNN -- docker compose pull && up -d
(pour LXC 103 : images via Zot 192.168.1.107:5000)
Zot registry (192.168.1.107:5000)
→ pull-through Docker Hub, GHCR, lscr.io onDemand
→ registry-mirrors sur daemon.json de chaque LXC
Structure monorepo /mnt/lxc-data (= homelab/infra)¶
/mnt/lxc-data/
├── .gitignore
├── .forgejo/workflows/
│ ├── renovate.yml # cron 06h00 + workflow_dispatch
│ └── deploy.yml # trigger push main → SSH deploy
├── renovate.json5 # majors=review, minor/patch=automerge 3j
├── 100-media/
│ ├── docker-compose.yml # images épinglées
│ ├── stack.env # GITIGNORED — PROWLARR_API_KEY
│ └── stack.env.example # template versionné
├── 101-admin/
│ └── docker-compose.yaml
├── 103-network/
│ ├── docker-compose.yml # Cup via Zot mirror pour GHCR
│ └── stack.env.example
├── 104-services/
│ └── docker-compose.yml
├── 105-web/
│ └── docker-compose.yml
├── 106-storage/
│ ├── docker-compose.yml
│ ├── stack.env # GITIGNORED — SFTPGO_OIDC secrets
│ └── stack.env.example
└── 107-forge/
├── docker-compose.yml # Forgejo + Runner + Zot
└── zot/config.json
Phases d'implémentation¶
Phase 0 — Inventaire versions images¶
Relevé docker inspect sur chaque LXC pour remplacer :latest par tags sémantiques.
Phase 1 — LXC 107 "forge" : Forgejo + Zot¶
- Création LXC 107 (Debian 12, 2GB RAM, 30GB,
192.168.1.107) 107-forge/docker-compose.yml: Forgejo (3000), Runner, Zot (5000)107-forge/zot/config.json: pull-through docker.io/ghcr.io/lscr.io- Caddy :
git.ncls.ltd → 192.168.1.107:3000(pas de SSO — Forgejo gère sa propre auth) - Authelia :
git.ncls.ltdenbypass(Git HTTP + API ne fonctionnent pas derrière SSO) daemon.jsonregistry-mirrorssur LXC 100/101/103/104/105/106
Phase 2 — Monorepo + images épinglées¶
git init /mnt/lxc-data, push vers Forgejo- Migration secrets →
stack.env(gitignored) - Bump toutes les images
:latest→ versions sémantiques (Phase 0) renovate.json5,stack.env.examplepar LXC.forgejo/workflows/: renovate.yml + deploy.yml
Phase 3 — Runner + déploiement Forgejo/Zot¶
- Déploiement LXC 107 via
pct exec 107 -- docker compose up -d - Enregistrement Forgejo Actions runner (token UI)
- SSH deploy key ED25519 →
authorized_keysProxmox host - Secrets Forgejo :
PROXMOX_HOST,PROXMOX_SSH_KEY,FORGEJO_RENOVATE_TOKEN,GITHUB_COM_TOKEN
Phase 4 — Validation end-to-end¶
- Test push → deploy workflow → container redémarré sur la bonne version
- Test Renovate dispatch → PR avec changelog
- Test auto-merge 3j (avec date forcée ou bypass)
Phase 5 — LXC 103 : Cup via Zot¶
- Image Cup dans
103-network/docker-compose.yml→192.168.1.107:5000/mirror/ghcr/sergi0g/cup:<version> - PackageRule Renovate pour suivi upstream ghcr.io
Phase 6 — Backup + Documentation¶
backup-docker-configs.sh: ajout107-forge/+*/stack.env- Docs :
applications/forgejo.md,applications/zot.md,infrastructure/.../107-forge.md - Mise à jour
index.md,proxy.md,sso.md,updates/docker.md
Inventaire images (Phase 0)¶
| LXC | Service | Image courante | Version épinglée |
|---|---|---|---|
| 100 | jellyfin | jellyfin/jellyfin:latest | 10.11.6 |
| 100 | sonarr | linuxserver/sonarr:latest | lscr.io/linuxserver/sonarr:4.0.16.2944-ls301 |
| 100 | radarr | linuxserver/radarr:latest | lscr.io/linuxserver/radarr:6.0.4.10291-ls290 |
| 100 | prowlarr | linuxserver/prowlarr:latest | lscr.io/linuxserver/prowlarr:2.3.0.5236-ls135 |
| 100 | bazarr | lscr.io/linuxserver/bazarr:latest | 1.5.4-ls333 |
| 100 | recyclarr | ghcr.io/recyclarr/recyclarr | 7.5.2 |
| 100 | sabnzbd | linuxserver/sabnzbd:latest | lscr.io/linuxserver/sabnzbd:4.5.5-ls241 |
| 100 | seerr | ghcr.io/seerr-team/seerr:latest | 3.1.0 |
| 100 | portainer-agent | portainer/agent:latest | 2.33.6 |
| 100 | cup | ghcr.io/sergi0g/cup:latest | 3.5.1 |
| 101 | portainer | portainer/portainer-ce:latest | :latest (pas de label version) |
| 101 | homepage | ghcr.io/gethomepage/homepage:latest | v1.9.0 |
| 101 | coolercontrol | coolercontrol/coolercontrold:latest | :latest (pas de label version) |
| 101 | zensical-builder | zensical/zensical:0.0.31 | 0.0.31 (déjà épinglé) |
| 101 | zensical (nginx) | nginx:alpine | :alpine (tag non-semver, skip Renovate) |
| 101 | cup | ghcr.io/sergi0g/cup:latest | 3.5.1 |
| 103 | caddy | caddy:2-alpine | 2.11.2-alpine |
| 103 | authelia | authelia/authelia:latest | 4.39.15 |
| 103 | wireguard | linuxserver/wireguard:latest | lscr.io/linuxserver/wireguard:1.0.20250521-r0-ls99 |
| 103 | portainer-agent | portainer/agent:latest | 2.33.6 |
| 103 | cup | ghcr.io/sergi0g/cup:latest | 192.168.1.107:5000/ghcr/sergi0g/cup:3.5.1 (via Zot) |
| 104 | portainer-agent | portainer/agent:latest | 2.33.6 |
| 104 | cup | ghcr.io/sergi0g/cup:latest | 3.5.1 |
| 105 | portainer-agent | portainer/agent:latest | 2.33.6 |
| 105 | cup | ghcr.io/sergi0g/cup:latest | 3.5.1 |
| 106 | sftpgo | drakkan/sftpgo:v2.7.1 | v2.7.1 (déjà épinglé) |
| 106 | cup | ghcr.io/sergi0g/cup:latest | 3.5.1 |
Note : torznab-proxy (LXC 100) = image build locale, Renovate gère le FROM du Dockerfile.
Conclusion¶
Date de fin : 2026-05-07
Implémentation complète — toutes les étapes exécutées sans intervention manuelle via l'agent Copilot sur root@pve.
Récapitulatif de l'implémentation complète¶
| Étape | Statut | Notes |
|---|---|---|
| Phase 0 — Inventaire versions | ✅ | Tableau ci-dessus, versions épinglées dans tous les compose |
| Phase 1 — Fichiers LXC 107 | ✅ | docker-compose.yml, zot/config.json, runner/config.yml |
| Phase 1 — Scripts provisioning | ✅ | provision-lxc107.sh, setup-registry-mirrors.sh |
| Phase 1 — Caddy + Authelia config | ✅ | Bloc git.ncls.ltd dans Caddyfile, bypass dans Authelia |
| Phase 2 — Monorepo config | ✅ | .gitignore, renovate.json5, workflows CI/CD |
| Phase 2 — Épinglage images | ✅ | Tous les docker-compose.yml LXC 100-106 |
| Phase 2 — Migration secrets | ✅ | stack.env LXC 100 et 106, env_file dans compose |
| Phase 6 — Backup script | ✅ | Entrées 107-forge et stack.env ajoutées |
| LXC 107 — création | ✅ | Debian 12 unprivileged, 2c/2GB, 192.168.1.107, bind mount |
| LXC 107 — Docker CE | ✅ | Docker 28.x installé via get.docker.com |
| Forgejo v15.0.1 | ✅ | Up et healthy, app.ini configuré, Actions activées |
| Zot registry | ✅ | Up, port 5000, pull-through docker.io/ghcr.io/lscr.io |
| Forgejo admin + org + repo | ✅ | Admin ncls, org homelab, repo infra |
| SSH deploy key | ✅ | ED25519 /root/.ssh/forgejo_deploy, dans Forgejo (Key ID 1) |
| Secrets Forgejo | ✅ | PROXMOX_SSH_KEY + RENOVATE_TOKEN (HTTP 201) |
| Git init + push initial | ✅ | 18 fichiers, commit 1d15912, branch main |
| Runner enregistré | ✅ | pve-runner v6.4.0, label ubuntu-latest, statut idle |
| Registry mirrors LXC 100-106 | ✅ | daemon.json http://192.168.1.107:5000 sur tous les LXC |
| Caddy reload | ✅ | Bloc git.ncls.ltd actif, HTTP→HTTPS 308 confirmé |
| Authelia reload | ✅ | Bypass git.ncls.ltd actif, statut healthy |
Correctifs appliqués pendant l'implémentation¶
| Problème | Cause | Solution |
|---|---|---|
| Zot config invalide | prefix interdit au niveau registry en Zot v2 |
Supprimé — seul content[].prefix est valide |
| Forgejo UID mismatch | LXC non-privilégié : UID 1000 container = 101000 host | chown 101000:101000 sur forgejo/data |
INSTALL_LOCK = false |
Formulaire installation non POSTé | sed -i pour passer à true + génération SECRET_KEY |
FORGEJO_ prefix réservé |
Forgejo refuse les secrets préfixés FORGEJO_ |
Renommé en RENOVATE_TOKEN dans secret + workflow |
Runner permission denied /data/.runner |
Runner image UID 1000, répertoire owned UID 100000 | chown 101000:101000 runner dir + déplacement .runner dans data/ |
| Runner socket docker permission denied | Runner image UID 1000, socket appartient à root:docker |
user: root dans docker-compose pour le runner |
| Zot healthcheck unhealthy | Container distroless — pas de wget/curl/shell |
healthcheck: disable: true |
.runner mauvais répertoire |
Registration montait runner/ mais compose monte runner/data/ |
mv runner/.runner runner/data/.runner |
État final (2026-05-07)¶
LXC 107 — 192.168.1.107
├── forgejo:15 (port 3000 HTTP / 2222 SSH) → healthy ✅
├── forgejo-runner:6 (label: ubuntu-latest) → idle ✅
└── zot-linux-amd64 (port 5000) → up ✅
git.ncls.ltd → Caddy (LXC 103) → Forgejo → 308 redirect confirmé ✅
Registry mirrors → Zot → configurés LXC 100/101/103/104/105/106 ✅
Authelia bypass git.ncls.ltd → healthy ✅
Monorepo /mnt/lxc-data → homelab/infra sur Forgejo → 3 commits ✅
Prochaines étapes recommandées¶
- Ajouter
GITHUB_COM_TOKENdans les secrets Forgejo : optionnel, évite le rate-limiting Docker Hub metadata lors des scans Renovate - Déclencher Renovate manuellement : workflow → "Run workflow" pour valider les PRs de mise à jour
- Tester le workflow deploy : pousser une modif sur un compose → vérifier le déclenchement CD
- Configurer
stabilityDaysdansrenovate.json5si les PRs arrivent trop vite (déjà à 3 jours dans le config)
Correctifs post-déploiement — 2026-05-12¶
Tags images — préfixe v manquant¶
Les tags relevés en Phase 0 ne reflétaient pas le format réel des registries ghcr.io et lscr.io :
| Service | Tag initial | Tag corrigé |
|---|---|---|
cup (tous LXC) |
3.5.1 |
v3.5.1 |
bazarr (LXC 100) |
1.5.4-ls333 |
v1.5.4-ls333 |
seerr (LXC 100) |
3.1.0 |
v3.1.0 |
Renovate — auth manquante¶
Le runner Renovate échouait avec 401. Le PAT renovate-bot avait été créé sans le scope read:user, requis par Renovate pour s'identifier. Recréé avec les scopes complets (write:repository, write:issue, read:organization, read:misc, read:user). Secret RENOVATE_TOKEN mis à jour.
Zot mirror — inaccessible depuis le runner¶
L'image 192.168.1.107:5000/ghcr/sergi0g/cup:v3.5.1 (LXC 103) est inaccessible depuis le runner car il tourne dans LXC 107. Règle ajoutée dans renovate.json5 pour désactiver Renovate sur toute référence 192.168.1.107:5000/*.
Migration — per-app subdirectories¶
Les stacks Portainer ont été migrées vers le monorepo GitOps avec une structure par sous-répertoire :
| Service | Stack Portainer | Nouveau chemin GitOps |
|---|---|---|
| endurain | #14 (LXC 104) | 104-services/endurain/docker-compose.yml |
| freshrss | #20 (LXC 104) | 104-services/freshrss/docker-compose.yml |
| romm | #23 (LXC 104) | 104-services/romm/docker-compose.yml |
| uptime-kuma | #34 (LXC 101) | 101-admin/uptime-kuma/docker-compose.yml |
| silverbullet | #36 (LXC 101) | 101-admin/silverbullet/docker-compose.yml |
Le deploy.yml iterate désormais les sous-répertoires après le compose racine de chaque LXC, via un script deploy.sh copié dans le LXC.
Bug deploy — quoting bash -c¶
La boucle for appdir in */ dans le inline bash -c de deploy.yml était inopérante : la variable $appdir était expandée à vide par le shell SSH avant d'atteindre le bash -c. Les sous-stacks n'étaient jamais déployés par CI.
Correctif : deploy.sh (script Shell autonome en racine du monorepo) est copié dans le LXC via pct push puis exécuté directement — sans imbrication de shell, sans ambiguïté de quoting.
Correctifs post-déploiement (session 2 — 2026-05-07)¶
Problèmes rencontrés et solutions¶
1. LXC 107 — daemon.json manquant → pull renovate/renovate timeout¶
Le workflow renovate.yml échouait silencieusement : l'image renovate/renovate était tirée directement depuis docker.io, sans passer par Zot, car LXC 107 n'avait pas de daemon.json configuré.
Solution : Création de /etc/docker/daemon.json sur LXC 107 :
{
"registry-mirrors": ["http://127.0.0.1:5000"],
"insecure-registries": ["127.0.0.1:5000", "192.168.1.107:5000"]
}
Complément : Image Renovate épinglée à renovate/renovate:43.169.1 dans renovate.yml (était :latest) — Zot télécharge l'image complète au premier appel (onDemand), les suivants sont servis immédiatement depuis le cache.
2. appleboy/ssh-action non résolu depuis le runner → suppression de tous les uses:¶
Les actions Forgejo hébergées sur data.forgejo.org ne sont pas résolvables depuis les containers de jobs (DNS interne Docker 127.0.0.11 ne résout pas les domaines externes). Tout uses: appleboy/ssh-action@... ou uses: actions/checkout@v4 échouait.
Solution : Suppression de tous les uses: dans deploy.yml. Remplacement par une étape run: native utilisant ssh standard et bash << EOF directement sur l'hôte Proxmox — pas d'action externe nécessaire.
3. Utilisateur dédié renovate-bot¶
Contexte : L'utilisation du compte admin nico pour Renovate est une mauvaise pratique (permissions excessives).
Solution :
- Création de l'utilisateur renovate-bot (ID 3, renovate-bot@ncls.ltd, non-admin)
- Génération d'un PAT avec scopes minimaux : write:repository, write:issue, read:organization, read:misc
- Secret Forgejo RENOVATE_TOKEN mis à jour (HTTP 204)
- Collaborateur write sur homelab/infra (HTTP 204)
- renovate.json5 mis à jour :
- "platform": "forgejo" (était "gitea")
- "gitAuthor": "Renovate Bot <renovate-bot@ncls.ltd>"
- "reviewers": ["nico"]
4. Runner v6 → v12 (deadlock auto-update)¶
Chronologie :
1. Renovate ouvre PR #1 : bump runner code.forgejo.org/forgejo/runner:6 → :12
2. PR mergée → déclenche deploy.yml sur le push main
3. deploy.yml exécute pct exec 107 -- docker compose up -d pour 107-forge
4. Le runner exécute son propre job depuis l'intérieur de son container → docker compose up -d tue et recrée le container du runner → job tué en plein milieu
5. Runner arrêté. Redémarrage manuel : pct exec 107 -- bash -c "cd /opt/docker && docker compose up -d forgejo-runner"
Solution : Exclusion de 107-forge du workflow deploy.yml :
- paths: trigger restreint à 1[0-6][0-9]-*/docker-compose.yml (exclut 107-*)
- regex grep -oP mise à jour en conséquence
- Entrée 107-forge) retirée du case
Procédure manuelle pour LXC 107 :
pct exec 107 -- bash -c 'cd /opt/docker && docker compose pull && docker compose up -d --remove-orphans'
Premiers PRs Renovate ouverts¶
| PR | Contenu | Statut |
|---|---|---|
| #1 | runner forgejo/runner:6 → :12 |
✅ Mergé |
| #2 | authelia 4.39.15 → 4.x |
🔄 En attente review |
| #3 | jellyfin 10.11.6 → 10.x |
🔄 En attente review |
| #4 | portainer-agent 2.33.6 → 2.41.0 |
🔄 Stabilité 3j |
État final stabilisé¶
LXC 107 — 192.168.1.107
├── forgejo:15 (port 3000/2222) → healthy ✅
├── forgejo-runner:12 (label: ubuntu-latest, user: root) → idle ✅
└── zot-linux-amd64 (port 5000, healthcheck disabled) → up ✅
Pipeline GitOps complet :
Renovate (cron 06h00) → PRs sur homelab/infra
Merge → deploy.yml → pct exec NNN -- docker compose up -d
107-forge : mise à jour MANUELLE uniquement