Skip to content

About

Reclaim disk space on a Linux server when a filesystem crosses a threshold. Not a logrotate: usage-driven, covers logs, journal, package caches, container images; truncates active files instead of deleting them. Dry run unless you ask.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

diskwarden

diskwarden

Reclaim disk space on a Linux server when a filesystem crosses a threshold — and not a moment before.

$ diskwarden --mountpoint /
2026-10-06T09:12:03Z [INFO] 62% of / used, below 70% — nothing to do
$ diskwarden
2026-10-06T14:40:11Z [INFO] === ROUTINE — 74% used, free 21GiB, DRY RUN ===
2026-10-06T14:40:11Z [INFO] would remove 1184 file(s), 6.1GiB — archived logs older than 5d
2026-10-06T14:40:12Z [INFO] would remove 61 file(s), 2.4GiB — rotated logs older than 7d
2026-10-06T14:40:12Z [INFO] would run: journalctl --vacuum-time=5d
2026-10-06T14:40:12Z [INFO] would run: docker image prune (older than 5d)
2026-10-06T14:40:12Z [INFO] === dry run complete — nothing was modified. Re-run with --apply ===

It is not a logrotate

logrotate rotates configured log files on a schedule. diskwarden watches a filesystem's usage percentage and acts only when it has to, across everything that quietly fills a server:

logrotate diskwarden
trigger time, or a size you configured per file the filesystem crossing a threshold
scope log files you listed logs, journal, package cache, /tmp, crash dumps, container images and build caches
active files rotates them, carefully leaves them alone — until an emergency threshold, where it truncates
unknown files ignores them *.log.<suffix> anywhere under /var/log is a rotated copy, no config needed

They are complements, not alternatives. Keep logrotate. diskwarden is for the night it was not enough.

Two thresholds

      < 70 %   nothing happens. No logging, no work, no risk.

  70 – 79 %   ROUTINE
              archived and rotated logs, journal vacuum, package cache,
              old /tmp files, crash dumps, orphaned container images,
              CI runner cache volumes

      ≥ 80 %   ROUTINE, and then:
              truncate active log files over 300 MB
              truncate container JSON logs over 100 MB

Both numbers are configurable. The gap between them matters: routine work is safe enough to run hourly and unattended, truncating a file somebody is writing to is not, and the two should not share a trigger.

Docs: https://allan-nava.github.io/diskwarden/ · Plan: BACKLOG.md → ROADMAP.md — generated from the script and the role defaults, so it cannot document a default that does not exist.

Install

sudo install -m 0755 diskwarden /usr/local/sbin/diskwarden
sudo install -m 0644 diskwarden.conf.example /etc/diskwarden.conf
sudo $EDITOR /etc/diskwarden.conf

# Look at what it would do, on the real machine, before anything is scheduled.
sudo diskwarden

# Only when you agree with the plan:
sudo install -m 0644 systemd/diskwarden.* /etc/systemd/system/
sudo systemctl enable --now diskwarden.timer

--apply lives in the systemd unit and nowhere else. There is no config key that turns deletion on, and no default that deletes. The only way diskwarden removes a file is a unit you installed on purpose.

Four things it knows that a one-line find does not

Truncate active files, never delete them. A process holding a log open keeps writing to the inode after you delete it, so the space is not returned until it restarts. df stays full while du looks fine, and only lsof +L1 explains why. Deleting a 40 GB log that a running service owns is how you turn a disk alert into an outage.

An active log ends in .log; anything with a suffix after it is a copy. app.log is being written. app.log.2026-10-01, app.log.3, app.log.gz are not. That one rule removes rotated logs that no logrotate config covers — the ones an application rotates itself, into a directory nobody declared — without ever touching a live file.

docker volume prune is too wide. It is happy to take a database volume with it. CI runner caches are removed by name instead (runner-*-cache-*), and one attached to a running job simply fails to remove, which is the correct outcome.

Below the threshold, do nothing — including logging. A cleanup tool that writes a line every hour to say it had nothing to do is a cleanup tool that fills the disk it is watching.

Testing

bash test/run-tests.sh            # the script
bash test/run-container-tests.sh  # the containers module, against a fake CLI
bash test/run-ansible-tests.sh    # the role
ansible-lint ansible/             # profile: production, a gate not advice

The container suite strips every real docker and podman from PATH and refuses to run if one is still reachable. That guard exists because writing it, one case had no shim, the host had a working daemon, and --apply ran docker image prune -a -f against it for real. A test suite that can issue destructive commands has to prove it is sandboxed before it issues any.

The tests do not prove it deletes files. That is easy, and most people manage it by accident. They prove the opposite:

  • below the threshold, nothing is touched and nothing is logged
  • without --apply, nothing is modified — ever, at any usage level
  • routine mode removes rotated copies and leaves every active log intact
  • emergency mode truncates active files and still never deletes one
  • an explicit flag beats the config file

A cleanup tool whose dry run you cannot trust is worse than no cleanup tool.

Run it against a fake tree yourself:

diskwarden --root /tmp/fake-server --usage 95 --modules "logs tmp"

--root prefixes every path and --usage overrides the measurement, so you can reproduce a full disk without having one.

Ansible

- role: diskwarden
  vars:
    diskwarden_routine_threshold: 70
    diskwarden_extra_log_dirs: [/opt/myapp/logs]
    diskwarden_enabled: false      # install now, arm after a dry run

The role copies the script rather than templating it, so the file in the repository is the file on the host, byte for byte, and the script stays runnable and testable without Ansible. It ships disarmed: installing diskwarden and arming it are two decisions.

ansible-playbook -i inventory ansible/playbook.example.yml
ssh host 'sudo diskwarden'                       # read the plan first
ansible-playbook ... -e diskwarden_enabled=true  # then arm it

ansible-lint runs as a gate, at the production profile, pinned in .ansible-lint. Pinning matters: without it the strictness is whatever the installed version defaults to, so a green run could mean the bar moved rather than the code being fine.

bash test/run-ansible-tests.sh checks the thing that silently breaks: that the role's variable names and the script's config keys still agree. Rename one side and nothing errors — the config is written, the script sources it, and the default quietly applies instead of your value.

What it does not do yet

Three limitations of 0.1.0, written down rather than discovered:

  • The dry run cannot put a number on every module. journalctl --vacuum, apt-get clean and docker prune report "would run" with no byte figure, because the only way to know is to run them. The file-based modules do give real numbers. (DW-9)
  • The containers module is tested against a fake CLI, not a real daemon. 17 cases prove the decision logic — above all that docker volume prune is never issued and that only runner-*-cache-* volumes are removed. What they cannot prove is that the commands are still valid, which a future Docker release could change. (DW-10 done, DW-18 open)
  • df lies on copy-on-write filesystems. On btrfs and ZFS, free space and reclaimable space are different questions and snapshots hold deleted data, so the thresholds would fire late or never. (DW-11)

All three are in BACKLOG.md for v0.2.0.

Modules

Each reclaimer is a no-op when the thing it manages is not installed, so one config works across a mixed fleet.

module what it reclaims
logs archived and rotated logs under /var/log, plus any EXTRA_LOG_DIRS
journal journalctl --vacuum-time
packages apt / dnf / yum caches
tmp old files in /tmp, then empty directories
crashdumps /var/crash, /var/lib/systemd/coredump
containers Docker or Podman images, containers, build cache; CI runner cache volumes by name

Adding one is a shell function called reclaim_<name> and a word in MODULES.

Status

Extracted from an Ansible role that has been running hourly on a production fleet, rewritten as a standalone script with a dry-run default, a config file instead of templating, and the test suite above.

The reclaim logic is the same logic. The packaging, the safety default and the tests are new, and the fleet-specific parts (a particular CI server's cache layout, a particular monitoring agent's logrotate) were dropped rather than generalised — they belong in EXTRA_LOG_DIRS or in your own module.

MIT. Issues and pull requests welcome.

About

Reclaim disk space on a Linux server when a filesystem crosses a threshold. Not a logrotate: usage-driven, covers logs, journal, package caches, container images; truncates active files instead of deleting them. Dry run unless you ask.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages