RFC: Automated Docker Image Updates
Status: Draft Date: 2026-04-27
Problem
With 54 Docker images across 35 services (now all pinned per ADR-017), manually discovering and applying version updates is unsustainable. Updates fall behind, security patches go unnoticed, and there's no structured process for testing before deploying.
Requirements
- Detect new upstream image versions automatically
- Present changes for human review before anything deploys
- Approve/reject from mobile (ntfy notification with action buttons)
- Full audit trail of every version change in git
- Safety controls: stability delay, major version locks, grouped updates for related images
- Fit the IaC pattern: everything through Forgejo Actions, Vault, Caddy, Alloy
Nice-to-haves
- Dashboard widget showing pending updates (Glance)
- Changelog summary in notifications
- Automatic image mirroring to Forgejo registry before deploy
- Open WebUI conversational interface for querying update status
Architecture
Components
All components already exist in the homelab except Renovate:
| Component | Role | Status |
|---|---|---|
| Renovate | Scans docker-compose files, creates PRs with version bumps | New — needs deployment |
| Forgejo | Hosts PRs, triggers workflows on merge | Existing (LXC 100) |
| n8n | Orchestrates approval flow: receives webhooks, sends notifications, merges PRs | Existing (LXC 120) |
| ntfy | Push notifications with approve/reject action buttons | Existing (LXC 119) |
| Glance | Dashboard widget showing pending Renovate PRs | Existing (LXC 119) |
| Forgejo Actions | Deploys on merge via Ansible playbooks | Existing (LXC 101) |
Flow
Renovate (hourly cron)
│
▼ scans docker-compose files in chizuru-v2
│ detects: immich v2.7.5 → v2.8.0 available
│ waits 3 days (stabilityDays)
│
▼ creates PR in Forgejo
│ title: "chore(deps): update immich to v2.8.0"
│ body: version diff, release notes link
│
▼ Forgejo webhook → n8n workflow
│
▼ n8n sends ntfy notification:
│ ┌──────────────────────────────────┐
│ │ Update: immich v2.7.5 → v2.8.0 │
│ │ 2 images, 1 compose file │
│ │ │
│ │ [Approve] [Reject] [View PR] │
│ └──────────────────────────────────┘
│
▼ user taps "Approve"
│
▼ ntfy POSTs to n8n approval webhook
│
▼ n8n merges PR via Forgejo API
│ POST /api/v1/repos/holo/chizuru-v2/pulls/{id}/merge
│
▼ merge triggers Forgejo Actions workflow
│ (paths: services/immich/**)
│
▼ workflow deploys via Ansible
│
▼ n8n sends confirmation ntfy:
"immich v2.8.0 deployed successfully"
Renovate Configuration
Deployment
Run Renovate as a scheduled Forgejo Actions workflow in a dedicated repo (e.g., holo/renovate-config). No new LXC needed.
# .forgejo/workflows/renovate.yml
name: Renovate
on:
schedule:
- cron: '0 */6 * * *' # every 6 hours
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Run Renovate
uses: renovatebot/[email protected]
env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
with:
configurationFile: config.js
Global Config (config.js)
module.exports = {
platform: 'forgejo',
endpoint: 'https://git.eva-00.network/api/v1/',
token: process.env.RENOVATE_TOKEN,
gitAuthor: 'Renovate Bot <[email protected]>',
repositories: ['holo/chizuru-v2'],
automerge: false,
platformAutomerge: false,
labels: ['renovate'],
packageRules: [
// Wait 3 days before proposing any update
{
matchDatasources: ['docker'],
stabilityDays: 3
},
// Lock major versions for critical infra
{
matchPackageNames: [
'netbirdio/management', 'netbirdio/signal', 'netbirdio/relay',
'qmcgaw/gluetun', 'postgres'
],
matchUpdateTypes: ['major'],
enabled: false
},
// Group Netbird images into one PR
{
groupName: 'netbird',
matchPackagePatterns: ['^netbirdio/', '^coturn/']
},
// Group Immich images into one PR
{
groupName: 'immich',
matchPackagePatterns: ['^ghcr.io/immich-app/']
},
// Group Gluetun + qBittorrent (dlbox)
{
groupName: 'dlbox-vpn',
matchPackageNames: ['qmcgaw/gluetun', 'lscr.io/linuxserver/qbittorrent'],
matchFileNames: ['services/gluetun/**']
},
// Group Tube Archivist stack
{
groupName: 'tube-archivist',
matchPackagePatterns: ['^bbilly1/tubearchivist', '^redis/redis-stack']
}
]
};
Repository Config (renovate.json in chizuru-v2)
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
}
n8n Approval Workflow
Trigger: Forgejo PR webhook
Forgejo sends a webhook on PR create/update to n8n.
n8n workflow steps:
- Receive webhook — filter for PRs with
renovatelabel - Extract details — PR title, body, changed files, URL
- Send ntfy notification with action buttons:
Title: {{ pr.title }} Body: {{ pr.body | truncate(200) }} Actions: http, Approve, https://n8n.eva-00.network/webhook/renovate-approve, method=POST, headers.Authorization=Bearer {{ token }}, body={"pr_number": {{ pr.number }}, "repo": "{{ pr.repo }}"} http, Reject, https://n8n.eva-00.network/webhook/renovate-reject, method=POST, headers.Authorization=Bearer {{ token }}, body={"pr_number": {{ pr.number }}} view, View PR, {{ pr.url }} - Wait for approval webhook (n8n Wait node)
- On approve: merge PR via Forgejo API
- On reject: close PR via Forgejo API, add comment
- Send confirmation ntfy notification with result
Glance Dashboard Widget
Add a custom-api widget to Glance showing pending Renovate PRs:
- type: custom-api
title: Pending Updates
cache: 5m
url: https://git.eva-00.network/api/v1/repos/holo/chizuru-v2/pulls?state=open&labels=renovate&token={FORGEJO_TOKEN}
template: |
{{ range .JSON.Array }}
{{ .title }} — {{ .created_at | parseTime "2006-01-02T15:04:05Z07:00" | toRelativeTime }}
{{ end }}
Open WebUI Integration (Future)
Once the core flow is working, add a Forgejo tool to Open WebUI that allows conversational queries:
- "What Renovate PRs are pending?"
- "Approve the Jellyfin update"
- "Show me the changelog for the Immich update"
This leverages the existing Forgejo MCP server or a custom Open WebUI tool calling the Forgejo API directly. Non-blocking for the initial deployment.
Rollout Plan
Phase 1: Renovate (Week 1)
- Create
renovatebot user on Forgejo with repo read/write permissions - Store PAT in Vault at
secret/renovate - Add
renovate.jsonto chizuru-v2 - Create
holo/renovate-configrepo with workflow + config.js - Deploy and verify PRs are created correctly
Phase 2: n8n + ntfy approval flow (Week 2)
- Build n8n workflow: webhook receiver → ntfy notification → wait → merge/close
- Configure Forgejo webhook for PR events → n8n
- Test end-to-end: Renovate PR → ntfy notification → tap approve → PR merged
Phase 3: Glance + polish (Week 3)
- Add Glance custom-api widget for pending PRs
- Add ntfy confirmation on successful deploy
- Monitor for false positives, tune packageRules
Phase 4: Open WebUI + registry mirror (Future)
- Open WebUI tool for conversational PR management
- Skopeo mirror step in deploy workflow (pull from Forgejo registry, not upstream)
Alternatives Considered
| Approach | Why not |
|---|---|
| Diun + manual | Too many services (54 images) for manual management |
| Watchtower | Bypasses IaC, auto-updates without review |
| Dependabot | No Forgejo support |
| FluxCD Image Automation | Requires Kubernetes |
| Open WebUI as approval UI | Chat interface, no push notifications or structured approve/reject flow |
Risks
- Renovate resource usage — runs periodically, bursty CPU/memory. Mitigated by running as a Forgejo Actions job (no persistent container).
- Too many PRs — 54 images could generate noise. Mitigated by
stabilityDays: 3, grouped updates, and locked major versions. - Accidental approval — fat-fingering approve on ntfy. Mitigated by n8n workflow requiring explicit PR merge via API (not just a notification dismissal).
- Forgejo API rate limits — Renovate scans may hit limits. Monitor and adjust cron frequency if needed.