GitOps with Forgejo, Komodo and Renovate¶
Every container on every server is defined by a compose.yaml in one git repository. Git is the source of
truth: I edit a file, push to main, and only the stacks whose files changed get redeployed. Nobody runs
docker compose up on a server by hand (with one exception, Komodo itself).
This section describes how that is put together, why each piece is there, and how to build it again from nothing.
The pieces¶
| Component | What it does | Runs where |
|---|---|---|
| Forgejo | Self-hosted git server. Holds the infra repo, sends a webhook on every push, hosts Renovate's PRs and the issue tracker. | control-host, as a Komodo stack |
| Komodo Core + MongoDB | Control plane: knows every server and stack, runs the deploy procedure, sends alerts. | control-host, compose applied by hand |
| Komodo Periphery | Agent on each server. Pulls the repo and runs docker compose when Core tells it to. |
container on control-host, systemd binary on server1, server2, workstation |
| Renovate | Watches every pinned image tag and opens a PR when a new version exists. Safe updates merge themselves. | control-host, as a Komodo stack |
| ntfy | Push notifications: failed deploys, down services, new PRs, finished deploys. | control-host |
Related pages: Secrets, Repo layout, Rebuild from scratch, Gotchas.
Architecture¶
flowchart LR
ws["workstation<br/>(edit + git push)"]
subgraph ch["control-host"]
fj["Forgejo<br/>infra repo"]
core["Komodo Core"]
db[("MongoDB")]
pch["Periphery<br/>(container)"]
ren["Renovate"]
ntfy["ntfy"]
end
subgraph s1["server1"]
p1["Periphery<br/>(systemd)"]
end
subgraph s2["server2"]
p2["Periphery<br/>(systemd)"]
end
ws -- "push main" --> fj
ren -- "PRs, automerge" --> fj
fj -- "push webhook" --> core
core --- db
core -- "deploy if changed" --> pch & p1 & p2
pch & p1 & p2 -- "git pull" --> fj
core -- "alerts" --> ntfy
fj -- "push / PR hooks" --> ntfy
Periphery runs in outbound mode: each agent opens the connection to Core, so the servers don't need an inbound port for Komodo.
How a change reaches a server¶
sequenceDiagram
autonumber
participant Me as workstation
participant FJ as Forgejo
participant KC as Komodo Core
participant P as Periphery (target host)
participant N as ntfy
Me->>FJ: git push (main)
FJ->>N: "pushed to main" (updates topic)
FJ->>KC: POST /listener/github/procedure/.../main
KC->>KC: Procedure "Redeploy Changed Stacks"<br/>BatchDeployStackIfChanged("*")
KC->>P: deploy stacks whose files changed
P->>FJ: git pull (one shared clone per host)
P->>P: write .env, docker compose up -d
KC->>N: deployed / failed (updates or alerts)
- Edit
hosts/<host>/<stack>/compose.yaml(or a config file the stack tracks) and push tomain. - Forgejo calls Komodo's webhook for the procedure Redeploy Changed Stacks.
- The procedure runs
BatchDeployStackIfChangedover every stack. Komodo compares each stack's files with what it last deployed and only redeploys the ones that differ. Unchanged stacks are not touched. - The same procedure also runs every 15 minutes on a schedule. If a deploy failed for a passing reason (a registry rate limit, a DNS blip, a dependency that came up slowly), the next run retries it with no push needed.
- Failures go to the ntfy
alertstopic. Deploys and PRs go toupdates.
Rollback is git revert <commit> and push. The same flow deploys the old version.
Why a procedure and not per-stack webhooks
Komodo can also give every stack its own webhook. With 20+ stacks in one repo that means 20+ webhooks firing on every push. One procedure webhook plus "deploy if changed" does the same with one hook, and the schedule turns it into a retry loop for free.
Image updates¶
Images are pinned to exact versions (image: ghcr.io/foo/bar:1.4.2, never :latest). Renovate opens a PR per
update. Patch and minor updates merge themselves in the daytime. Majors, databases and anything that can lock you out
wait for review. Merging is just another push to main, so the deploy flow above takes over. Details:
Renovate.
Komodo's own image polling stays on (poll_for_updates) only to show "update available" in the UI. Komodo never
updates an image by itself (auto_update = false), because then git would no longer describe what is running.
What lives outside this flow¶
| Thing | How it is managed | Why |
|---|---|---|
| Komodo Core / MongoDB / control-host Periphery | Compose in git, copied to the host and applied by hand | Komodo can't redeploy the thing running the deploy. See Komodo. |
| Host-level config (unattended-upgrades, smartd, disk-space checks, notify script) | Files in hosts/_fleet/, installed with an idempotent script over SSH |
Not containers. See Repo layout. |
| Secrets | Komodo secret Variables, never git | See Secrets. |
| The workstation's own stacks | Run by hand, ignored by Renovate | A desktop shouldn't redeploy itself while you're using it. |