Daily fstrim and LVM thin pool monitor for Proxmox, with Discord alerts before the pool fills and pauses every guest on it.
Plain bash, cron, and curl. No LLM, no gateway, no API credits in the loop, so it keeps working when everything else is on fire.
On 2026-05-05, pve/data on a Proxmox host hit 100% during a scheduled update and paused
a running VM with I/O errors. LVM thin pools overcommit space, and guests never hand
freed blocks back to the pool without a trim. Once a thin pool's data or metadata fills,
every guest on it freezes at once, not just the one that triggered it.
This watchdog runs every guest's fstrim first, so freed blocks actually get returned to the pool, then checks how full the pool is and posts to Discord if it crosses a threshold.
A normal run, from /var/log/pve-pool-watchdog.log:
2026-07-03 04:30:01 fstrim CT 101 ok
2026-07-03 04:30:02 fstrim CT 105 ok
2026-07-03 04:30:04 fstrim CT 113 ok
2026-07-03 04:30:07 fstrim VM 100 ok
2026-07-03 04:30:09 fstrim VM 104 skipped: guest agent is not running
2026-07-03 04:30:09 fstrim done: CTs ok=3 failed=0, VMs ok=1 skipped=1
2026-07-03 04:30:09 pool pve/data: data 62% | meta 41% | ~132G free of 348G [ok]
2026-07-03 04:30:09 all pools ok, no alert
A run that crosses the warning threshold sends a Discord message like:
:warning: **thin pool warning on `pve1`**
pve/data: data 83% | meta 52% | ~59G free of 348G [WARN]
Biggest thin volumes:
vm-100-disk-0 210G of 250G (84%)
vm-104-disk-0 95G of 150G (63%)
ct-113-disk-0 40G of 64G (63%)
fstrim: 3 CTs + 1 VMs trimmed (0 CT fail, 1 VM no-agent)
Worth a look before it becomes a 3am problem.
And past the critical threshold:
:rotating_light: **thin pool CRITICAL on `pve1`** (data >= 90% or meta >= 80%)
pve/data: data 94% | meta 61% | ~21G free of 348G [CRIT]
Biggest thin volumes:
vm-100-disk-0 235G of 250G (94%)
...
fstrim: 3 CTs + 1 VMs trimmed (0 CT fail, 1 VM no-agent)
If this fills, every guest on the pool pauses with I/O errors. Free space NOW: delete
old snapshots, unused disks, or move a disk off the pool.
- Trims every running LXC container (
pct fstrim) and every running VM with a QEMU guest agent (qm guest cmd fstrim) before checking pool usage, so freed blocks are actually returned before the check runs. - Reads data% and metadata% for every LVM thin pool on the host from
lvs. Metadata gets its own, tighter thresholds since a thin pool can break on full metadata even with data space to spare. - Discord alert at WARN and CRIT severity, listing every pool's numbers, the three biggest thin volumes by allocated space, and the fstrim tally for the run.
- Silent when everything is healthy. Only writes to the log, no alert noise.
- Per-guest fstrim timeout so one stuck container or VM can't hang the whole run.
--testforces a Discord post with current stats regardless of thresholds, for checking the webhook works end to end.--no-trimskips fstrim for a fast, report-only run.
- A Proxmox VE host you already run. This tool only makes sense on a real Proxmox host, it isn't something you install standalone. If you don't run Proxmox yet, this isn't the project to start with.
- Root SSH access to that host. Either a saved SSH key or the root password, whichever you normally use to reach it. Check you can log in with
ssh root@<your-host-ip>before going further. - Bash 4 or newer, which every current Proxmox VE install already has, so there's nothing to install here, just something the script needs.
- (Optional but recommended) a Discord webhook URL, so the watchdog can actually alert you instead of only writing to a log file. In Discord: Server Settings > Integrations > Webhooks > New Webhook, pick or make a channel (an "announcements" or "alerts" channel works well), then Copy Webhook URL. It looks like
https://discord.com/api/webhooks/123456789/AbCdEf.... Without this, the tool still runs and still logs, it just never posts anywhere. - A machine to run the deploy command from. Any Mac, Linux box, or Windows machine with an
sshandscpclient works (Windows 10/11 already ship both). This can be the same machine you're reading this on.
Get the code first:
git clone https://github.com/KitsuneTech1/pve-pool-watchdog.git
cd pve-pool-watchdogYou should see pve-pool-watchdog.sh, deploy.sh, and an etc/ folder in the directory.
deploy.sh copies the watchdog script, its cron schedule, and its log rotation config to any number of Proxmox hosts over SSH, in one command.
- Run it, listing every host you want the watchdog on (replace with your own root@ip addresses):
For each host, you should see
./deploy.sh root@pve1 root@pve2
deployed okprinted. If a host doesn't have a config file yet, you'll also see a note reminding you alerts are disabled until you add one, that's expected on a brand new host, handled in the next step. - This step doesn't touch
/etc/pve-pool-watchdog.confon purpose, so re-runningdeploy.shlater to update the script is always safe and never wipes out a webhook URL you already set. On a host you're deploying to for the first time, though, that config file doesn't exist yet, so alerts stay silent until you create it:scp pve-pool-watchdog.conf.example root@<host-ip>:/etc/pve-pool-watchdog.conf ssh root@<host-ip> 'chmod 600 /etc/pve-pool-watchdog.conf'
chmod 600means only root can read or write the file, which matters here because it holds your webhook URL. - Edit that file on the host (
ssh root@<host-ip>thennano /etc/pve-pool-watchdog.conf, or any editor you like) and fill inWEBHOOK_URL="https://discord.com/api/webhooks/..."with the URL from Discord. Leave every other line commented out unless you want to change the default thresholds, see the Config table below for what each one does.
It worked if: the command in "Verify end to end" below posts a message to your Discord channel.
Keep it running: nothing to set up, deploy.sh already installed a cron job (see Cron schedule below) that runs it automatically every day. There's no service to start or enable.
Use this if you'd rather see and run every step yourself, or deploy.sh doesn't work in your environment for some reason.
- Copy the three files to the host:
scp pve-pool-watchdog.sh root@HOST:/usr/local/sbin/pve-pool-watchdog.sh scp etc/cron.d/pve-pool-watchdog root@HOST:/etc/cron.d/pve-pool-watchdog scp etc/logrotate.d/pve-pool-watchdog root@HOST:/etc/logrotate.d/pve-pool-watchdog
- Set correct permissions and create the log file:
ssh root@HOST 'chmod 755 /usr/local/sbin/pve-pool-watchdog.sh chmod 644 /etc/cron.d/pve-pool-watchdog /etc/logrotate.d/pve-pool-watchdog touch /var/log/pve-pool-watchdog.log'
chmod 755makes the script executable.chmod 644makes the cron and logrotate files readable by the system but only root-writable, which is what those files expect. - Set up the config file the same way as step 2 in Option A (copy
pve-pool-watchdog.conf.exampleto/etc/pve-pool-watchdog.conf,chmod 600, fill inWEBHOOK_URL).
It worked if: the same "Verify end to end" command below posts to Discord.
etc/cron.d/pve-pool-watchdog runs the script daily at 04:30, as root, with a 30 minute
timeout, so it can never hang forever if something goes wrong. 04:30 is deliberately
ahead of most nightly game-server update crons, so the pool gets trimmed and checked
right before its riskiest hour.
There's no systemd unit for this project, and you don't need one. Cron is enough for a once-a-day job, and it's one less thing that has to stay running in the background.
ssh root@HOST '/usr/local/sbin/pve-pool-watchdog.sh --test --no-trim'--test forces a Discord post with the current pool numbers even when everything is
healthy, so you don't have to wait for a real warning to know the webhook works.
--no-trim skips the fstrim step so this check runs in a couple of seconds.
It worked if: you see a message show up in your Discord channel within a few seconds, formatted like the examples in the Demo section above.
- Nothing shows up in Discord, but the script runs without errors:
WEBHOOK_URLis probably still empty in/etc/pve-pool-watchdog.conf. The script treats an empty webhook as "log only, don't alert" on purpose, so this can look like success when it isn't. Check the file has the real URL and is on the host you tested, not just your local repo copy. deploy.shprints "NOTE: /etc/pve-pool-watchdog.conf missing, alerts disabled until created": this is expected on a host's first deploy, not an error. Follow step 2 in Option A to add the config file.- A VM shows up as "skipped: guest agent is not running": this is normal and not a failure. Only VMs with the QEMU guest agent installed and running can be trimmed from the host side; containers (CTs) don't need this. Install the guest agent inside that VM if you want it trimmed too, otherwise it's safe to ignore.
- The cron job never seems to run: check it actually landed with
cat /etc/cron.d/pve-pool-watchdogon the host, and that the file ends with a newline (cron silently ignores files that don't). Also check/var/log/pve-pool-watchdog.logfor a line from around 04:30 to confirm it's firing.
Copy pve-pool-watchdog.conf.example to /etc/pve-pool-watchdog.conf and chmod 600
it (it holds the webhook URL). This file is never committed to the repo, it's in
.gitignore on purpose.
| Key | Default | Meaning |
|---|---|---|
WEBHOOK_URL |
(empty) | Discord webhook to post alerts to. If empty, alerts are logged but not sent. |
HOST_LABEL |
$(hostname) |
Name shown in alert messages. |
DATA_WARN |
80 |
Pool data% that triggers a WARN alert. |
DATA_CRIT |
90 |
Pool data% that triggers a CRIT alert. |
META_WARN |
70 |
Pool metadata% that triggers a WARN alert. |
META_CRIT |
80 |
Pool metadata% that triggers a CRIT alert. |
CT_TRIM_TIMEOUT |
180 |
Seconds to wait for a single container's fstrim before giving up on it. |
VM_TRIM_TIMEOUT |
180 |
Seconds to wait for a single VM's fstrim before giving up on it. |
SKIP_FSTRIM |
0 |
Set to 1 to never fstrim, report only. Same effect as --no-trim. |
- fstrim phase. Lists running containers with
pct listand running VMs withqm list, then trims each one with a per-guest timeout. A VM without a guest agent fails cleanly and is counted as skipped, not fatal, that's the most common case. - Pool check. Reads
lvsfor every LVM thin pool (lv_attr =~ ^t) on the host, pulling data%, metadata%, and total size. Also pulls the three largest thin volumes by allocated space so an alert points at what's actually eating the pool. - Severity. Each pool is scored
ok,WARN, orCRITagainst the thresholds above. The worst severity across all pools decides whether an alert fires. - Alert. CRIT or WARN posts to the configured Discord webhook with per-pool
numbers, the top volumes, and the fstrim tally. Healthy runs write to the log only.
--testforces a post even when healthy, for checking the webhook end to end.
ssh root@HOST 'rm -f /usr/local/sbin/pve-pool-watchdog.sh /etc/cron.d/pve-pool-watchdog \
/etc/logrotate.d/pve-pool-watchdog /etc/pve-pool-watchdog.conf'MIT. Copyright (c) 2026 Kitsune Technologies LLC. Do what you like with it, commercially or otherwise, just keep the copyright notice. Full text in LICENSE.