[Infra][CI] Gitea Actions leaves GITEA-ACTION* volumes behind → build nodes recurrently fill up (need runner auto-cleanup / scheduled prune) #514

Open
opened 2026-07-12 19:58:40 +00:00 by spikerj · 7 comments
Owner

Summary

Build/CI nodes (e.g. laptop-server) recurrently run out of disk, and the confirmed culprit is leftover GITEA-ACTION* Docker volumes that the Gitea Actions runner creates per job and never cleans up. They accumulate indefinitely until the node fills and manual docker volume rm is needed. This has recurred multiple times (also intertwined with the separate GITEA-ACTIONS-* leftover-network / IPv4 address-pool exhaustion issue).

Immediate remediation (manual, per node)

docker volume ls -q --filter name=GITEA-ACTION | wc -l                       # scale
docker volume ls -q --filter name=GITEA-ACTION | xargs -r docker volume rm   # in-use skipped safely
docker system df                                                             # verify reclaim

Durable fixes (pick one or combine)

  1. Runner-level auto-cleanup — configure the Gitea Actions runner so job volumes are removed on completion. Depending on runner/version: enable container/volume auto-remove for the job executor, or set the runner's cleanup/GC option so per-task volumes don't persist after the job.
  2. Scheduled prune (belt-and-suspenders) — a systemd timer / cron on each build node:
    # daily, off-hours
    docker volume ls -q --filter name=GITEA-ACTION | xargs -r docker volume rm 2>/dev/null
    docker builder prune -f --filter until=48h
    docker image prune -f
    
    (Name-filtered volume rm avoids touching real named data volumes; rm skips any volume still attached to a running job.)
  3. Disk monitoring/alerting — alert at e.g. 80% on build nodes so this is caught before it wedges CI, rather than discovered when a job fails.

Impact

When a build node fills: CI jobs fail, and it can compound with the network-address-pool exhaustion (leftover GITEA-ACTIONS-* networks) to block new job containers entirely. Recurring manual toil to reclaim space.

Related

  • Separate but same-family: GITEA-ACTIONS-* network leftovers → could not find an available, non-overlapping IPv4 address pool (fix: docker network prune -f + daemon.json default-address-pools). Consider bundling both cleanups into the same scheduled job.
  • Disk-cleanup recipe kept in the on-call notes (build cache / images / volumes ordering).

Verification

  • After enabling runner auto-cleanup: run a few CI jobs, confirm docker volume ls --filter name=GITEA-ACTION does not grow (count returns to baseline after jobs finish).
  • After scheduled-prune: confirm the timer fires and disk stays under threshold across a day of CI activity.
## Summary Build/CI nodes (e.g. `laptop-server`) **recurrently run out of disk**, and the confirmed culprit is **leftover `GITEA-ACTION*` Docker volumes** that the Gitea Actions runner creates per job and never cleans up. They accumulate indefinitely until the node fills and manual `docker volume rm` is needed. This has recurred multiple times (also intertwined with the separate `GITEA-ACTIONS-*` leftover-network / IPv4 address-pool exhaustion issue). ## Immediate remediation (manual, per node) ```bash docker volume ls -q --filter name=GITEA-ACTION | wc -l # scale docker volume ls -q --filter name=GITEA-ACTION | xargs -r docker volume rm # in-use skipped safely docker system df # verify reclaim ``` ## Durable fixes (pick one or combine) 1. **Runner-level auto-cleanup** — configure the Gitea Actions runner so job volumes are removed on completion. Depending on runner/version: enable container/volume auto-remove for the job executor, or set the runner's cleanup/GC option so per-task volumes don't persist after the job. 2. **Scheduled prune (belt-and-suspenders)** — a systemd timer / cron on each build node: ``` # daily, off-hours docker volume ls -q --filter name=GITEA-ACTION | xargs -r docker volume rm 2>/dev/null docker builder prune -f --filter until=48h docker image prune -f ``` (Name-filtered volume rm avoids touching real named data volumes; `rm` skips any volume still attached to a running job.) 3. **Disk monitoring/alerting** — alert at e.g. 80% on build nodes so this is caught before it wedges CI, rather than discovered when a job fails. ## Impact When a build node fills: CI jobs fail, and it can compound with the network-address-pool exhaustion (leftover `GITEA-ACTIONS-*` networks) to block new job containers entirely. Recurring manual toil to reclaim space. ## Related - Separate but same-family: `GITEA-ACTIONS-*` **network** leftovers → `could not find an available, non-overlapping IPv4 address pool` (fix: `docker network prune -f` + `daemon.json` `default-address-pools`). Consider bundling both cleanups into the same scheduled job. - Disk-cleanup recipe kept in the on-call notes (build cache / images / volumes ordering). ## Verification - After enabling runner auto-cleanup: run a few CI jobs, confirm `docker volume ls --filter name=GITEA-ACTION` does not grow (count returns to baseline after jobs finish). - After scheduled-prune: confirm the timer fires and disk stays under threshold across a day of CI activity.
Author
Owner

Durable fix up: infra PR #48gitea-act-runner/cleanup/ with a name-filtered prune script (GITEA-ACTION* volumes + GITEA-ACTIONS-* networks + 48h build-cache + dangling images; real named volumes structurally untouchable; in-use resources skipped), systemd daily 04:30 timer, and a per-node install.sh that reclaims immediately on install.

Root-cause note: the runner configs already set container.rm: true, but the current act_runner nightly removes only job containers — the per-job workspace volumes leak regardless. The timer is the version-proof backstop; pinning act_runner past nightly when a stable tag ships volume cleanup is the complementary follow-up.

After merge, one command per runner node: cd /mnt/infrastructure/gitea-act-runner/cleanup && sudo bash install.sh — will close on your confirmation it ran (the journald lines show reclaimed counts).

Durable fix up: **infra PR #48** — `gitea-act-runner/cleanup/` with a name-filtered prune script (GITEA-ACTION* volumes + GITEA-ACTIONS-* networks + 48h build-cache + dangling images; real named volumes structurally untouchable; in-use resources skipped), systemd daily 04:30 timer, and a per-node `install.sh` that reclaims immediately on install. Root-cause note: the runner configs already set `container.rm: true`, but the current act_runner **nightly** removes only job containers — the per-job workspace volumes leak regardless. The timer is the version-proof backstop; pinning act_runner past nightly when a stable tag ships volume cleanup is the complementary follow-up. After merge, one command per runner node: `cd /mnt/infrastructure/gitea-act-runner/cleanup && sudo bash install.sh` — will close on your confirmation it ran (the journald lines show reclaimed counts).
Author
Owner

Escalation data point (2026-07-13 ~05:14Z): tonight this bit much harder than runner-volume leftovers — leftover ModelEnvImages layers/build-cache on laptop-server (two killed runs of the 98 GB sdxl build, see #537) filled / completely, which crash-looped gitea_postgres ('could not write lock file: No space left on device') and took down git hosting + API auth + CI task-fetch for the whole team (~25 min, until manual docker builder prune). Two hardening asks: (1) the prune timer from 4e3bf89 should also run docker builder prune with a size cap, not just GITEA-ACTION* volumes; (2) consider moving gitea_postgres's volume off the same filesystem the runners build on, or an alert at 85% — the DB of record shouldn't share disk headroom with 100 GB CI builds.

Escalation data point (2026-07-13 ~05:14Z): tonight this bit much harder than runner-volume leftovers — leftover ModelEnvImages layers/build-cache on laptop-server (two killed runs of the 98 GB sdxl build, see #537) filled / completely, which crash-looped **gitea_postgres** ('could not write lock file: No space left on device') and took down git hosting + API auth + CI task-fetch for the whole team (~25 min, until manual `docker builder prune`). Two hardening asks: (1) the prune timer from 4e3bf89 should also run `docker builder prune` with a size cap, not just GITEA-ACTION* volumes; (2) consider moving gitea_postgres's volume off the same filesystem the runners build on, or an alert at 85% — the DB of record shouldn't share disk headroom with 100 GB CI builds.
Author
Owner

Second disk-full within 6h (2026-07-13 ~08:03Z→~14:50Z): laptop-server filled again after the earlier ~05:15Z cleanup, killing gitea_postgres (and with it git/API/CI) for ~7h until this morning's prune. Two data points now for the durable fixes proposed above — builder-cache in the prune timer + postgres volume separation / disk alert. Also of note: the hung ModelEnvImages run 10784 (#537) died at 08:01Z, right as the disk filled — plausibly the runner's own cache growth was a contributor. Everything is healthy as of ~14:55Z.

Second disk-full within 6h (2026-07-13 ~08:03Z→~14:50Z): laptop-server filled again after the earlier ~05:15Z cleanup, killing gitea_postgres (and with it git/API/CI) for ~7h until this morning's prune. Two data points now for the durable fixes proposed above — builder-cache in the prune timer + postgres volume separation / disk alert. Also of note: the hung ModelEnvImages run 10784 (#537) died at 08:01Z, right as the disk filled — plausibly the runner's own cache growth was a contributor. Everything is healthy as of ~14:55Z.
Author
Owner

Hardening PR: spikersoft-infrastructure#58 — builder prune size cap (--keep-storage 60GB, LRU; the until=48h filter was actually exempting the hours-old outage cache), hourly timer, emergency full-cache drop at >=85% disk, exit-2 ALERT when even that can't get under threshold (means non-CI data, i.e. the postgres-headroom issue). Rollout = re-run install.sh per build node — on laptop-server wait for the in-flight artpipe pushes first. Postgres volume separation + a real disk alert (docker-monitor/node-agent) remain open.

Hardening PR: spikersoft-infrastructure#58 — builder prune size cap (--keep-storage 60GB, LRU; the until=48h filter was actually exempting the hours-old outage cache), hourly timer, emergency full-cache drop at >=85% disk, exit-2 ALERT when even that can't get under threshold (means non-CI data, i.e. the postgres-headroom issue). Rollout = re-run install.sh per build node — on laptop-server wait for the in-flight artpipe pushes first. Postgres volume separation + a real disk alert (docker-monitor/node-agent) remain open.
Author
Owner

Prune hardening MERGED (infra #58): size-capped hourly prune + emergency full-cache drop + exit-2 ALERT. Remaining: re-run install.sh on each build node (wait for in-flight artpipe pushes on laptop-server), plus the two open asks — gitea_postgres volume separation and a proper disk alert (docker-monitor/node-agent family).

Prune hardening MERGED (infra #58): size-capped hourly prune + emergency full-cache drop + exit-2 ALERT. Remaining: re-run install.sh on each build node (wait for in-flight artpipe pushes on laptop-server), plus the two open asks — gitea_postgres volume separation and a proper disk alert (docker-monitor/node-agent family).
Author
Owner

Board-sweep status (2026-07-22): prune automation merged (daily #48 + size-capped hourly + alert #58). REMAINING: re-run install.sh per build node, gitea_postgres volume separation, a proper disk alert.

Board-sweep status (2026-07-22): prune automation merged (daily #48 + size-capped hourly + alert #58). REMAINING: re-run install.sh per build node, gitea_postgres volume separation, a proper disk alert.
Author
Owner

Audited against origin/masterthe prune mechanism shipped; three items remain. Staying open.

Landed (infra PRs #48, #58): gitea-act-runner/cleanup/gitea-actions-prune.sh — name-filtered volume rm (:50-53), network rm (:55-58), docker builder prune -f --keep-storage (:70), and exit 2 as an alert signal (:84); plus gitea-actions-prune.timer (OnCalendar=hourly, :11), the .serviceunit, andinstall.sh`.

Remaining:

  1. Whether install.sh was actually run on each build node is unverifiable from git. There's no install record or node inventory in the repo, so a timer that exists in version control tells us nothing about whether it's active anywhere. Settling evidence: systemctl status gitea-actions-prune.timer on each runner node. Given the failure mode is silent (disks just fill again), this is worth confirming rather than assuming.

  2. The gitea_postgres volume separation was never done. gitea/docker-stack.yml:7 still points at /home/joey/docker/gitea/postgres/data — the same filesystem the runners build on. So a runaway build can still pressure the Gitea database's disk, which is the coupling that made #700 (Gitea storage.Init crash-loop → full git+CI outage) so damaging.

  3. No disk alerting. git grep -rin 'disk|usage' origin/master -- spikersoft-node-agent spikersoft-docker-monitor returns only journald lines from enable-persistent-journald.sh. No threshold alert exists, so item 1's silence has nothing backstopping it.

Item 2 is the one I'd prioritise — it's the difference between "builds fill a disk" and "builds take Gitea down". Related: #757 (root disk hit 99%, pruned 222.7 GB) and #700.

Audited against `origin/master` — **the prune mechanism shipped; three items remain.** Staying open. **Landed** (infra PRs #48, #58): `gitea-act-runner/cleanup/gitea-actions-prune.sh` — name-filtered `volume rm` (`:50-53`), `network rm` (`:55-58`), `docker builder prune -f --keep-storage` (`:70`), and `exit 2` as an alert signal (`:84); plus `gitea-actions-prune.timer` (`OnCalendar=hourly`, `:11`), the `.service` unit, and `install.sh`. **Remaining:** 1. **Whether `install.sh` was actually run on each build node is unverifiable from git.** There's no install record or node inventory in the repo, so a timer that exists in version control tells us nothing about whether it's active anywhere. Settling evidence: `systemctl status gitea-actions-prune.timer` on each runner node. Given the failure mode is silent (disks just fill again), this is worth confirming rather than assuming. 2. **The `gitea_postgres` volume separation was never done.** `gitea/docker-stack.yml:7` still points at `/home/joey/docker/gitea/postgres/data` — the same filesystem the runners build on. So a runaway build can still pressure the Gitea database's disk, which is the coupling that made #700 (Gitea storage.Init crash-loop → full git+CI outage) so damaging. 3. **No disk alerting.** `git grep -rin 'disk|usage' origin/master -- spikersoft-node-agent spikersoft-docker-monitor` returns only journald lines from `enable-persistent-journald.sh`. No threshold alert exists, so item 1's silence has nothing backstopping it. Item 2 is the one I'd prioritise — it's the difference between "builds fill a disk" and "builds take Gitea down". Related: #757 (root disk hit 99%, pruned 222.7 GB) and #700.
Sign in to join this conversation.