Stop the "disk full" 3 a.m. calls. A free, open-source FreePBX 16/17 module that watches every category of bloat a real-world Asterisk box accumulates — module cache, systemd journal, package cache, logs, recordings, backups — and trims each against admin-set retention knobs every night. No DB pruning, no destructive surprises, intruder-tracking logs preserved by default.
Run a fleet of FreePBX boxes for a few years and the same thing happens to every one of them: the root partition slowly fills up with files nobody knows are there, until one morning Asterisk can't write a CDR, voicemail recording fails mid-message, the backup module bombs out, and you're on a Sunday-morning call from a customer because dialtone went away.
None of this is touched by stock FreePBX. Sangoma's paid Sysadmin Pro module has a Storage page but it's a mountpoint UI for redirecting backups to network shares — it doesn't actually do per-category cleanup. Custom Disk is a free alternative that does. Realistic floor across a typical fleet: 3 GB freed per PBX, ~100 GB fleet-wide. None of it from the recordings or DB you actually care about.
Verified across a 330-PBX fleet audit before this module was written:
/var/cache/apt/archives — 1.2 GB of stale package downloads/var/log/journal — 1.4 GB (unbounded by default)admin/modules/_cache — 500–700 MB of every .tgz.gpg ever downloadedcore-fastagi_out.log-* — 1.5 GB from a deleted-but-open-fd writerfreepbx.log-* — ~100 MB/day on noisy PBXesCompatibility: FreePBX 16 and 17 on Debian (apt) or RHEL/Alma/Rocky (dnf). Auto-detects which package manager you have and uses the right cleaner.
Three cooperating layers, all running as the asterisk user under flock so a missed run auto-recovers within 60 seconds and overlap is impossible.
Rotates /var/log/asterisk/full, messages, and fail2ban-* logs — the files FreePBX's own /etc/logrotate.d/ snippets don't cover (Asterisk's logger module rotates them without a size cap). Compresses anything older than 7 days; never deletes within the intruder-tracking floor.
Walks all 10 cleanup categories, trims each against the configured retention knobs, and emails the admin if disk usage is still over the action threshold when the run finishes.
Reads root partition usage. If usage is at or above the emergency threshold (default 90%), spawns an out-of-band cleanup pass and sends an urgent email. Rate-limited to one extra pass per hour so it can't loop.
Writes are guarded by hard-coded safety rails the admin can't override: never delete from /etc/, never touch *.sqlite3/*.db/*.ibd files, never touch files owned by mysql, never touch /var/lib/asterisk/sounds/moh/keys, and a 30-day minimum on every intruder-tracking log file regardless of admin setting. If anything tries to delete during a FreePBX backup window (/var/lib/asterisk/backup.lock present), the whole run aborts.
Every category has its own retention knob. Defaults are sized to the partition; everything is editable.
| Category | What it does | Restart needed? |
|---|---|---|
| Module cache | Per-module: keep only the currently-installed version (configurable: keep N most-recent others too, 0 = wipe cache entirely) | No |
| systemd journal | journalctl --vacuum-size=<cap> — capped without editing journald.conf (vacuum runs nightly to keep size bounded) | No |
| Package cache | apt-get clean (Debian) or dnf clean all (RHEL) — only when current size > configured cap, and only when the UI toggle is enabled | No |
| FastAGI logs | Truncate-in-place any "hung-fd" core-fastagi_out.log-* (the deleted-but-open-fd pattern that grows to 1.5 GB), plus age-out dated rotated files | pm2 --reload-logs when a truncate happens |
| Asterisk logs | Delete rotated *.log-* files older than the knob, with asterisk -rx 'logger reload' if full*/messages* were touched | logger reload when needed |
| freepbx.log overflow | Tail-truncate each freepbx.log-* to the per-file cap (keeps the most recent content) | No |
| fail2ban logs | Delete rotated fail2ban-* files older than the knob — 30-day intruder-tracking floor enforced server-side | No |
| queue_log | Delete rotated queue_log-* older than the knob | No |
| Call recordings | Discover MIXMON_DIR from Asterisk globals, delete recordings older than the knob, prune empty year/month/day dirs. Offsite-policy aware. | No |
| FreePBX backups | Per backup-job directory, keep the N most-recent .tar.gz, prune the rest | No |
At install time the module reads disk_total_space('/') and seeds reasonable defaults proportional to the partition size. Defaults work on a 14 GB virtual PBX or a 500 GB hardware box without retuning.
| Knob | Default formula |
|---|---|
| Journal cap (MB) | min(2% of partition, 300 MB) floored at 100 MB |
| Module cache versions to keep | 1 (installed version only) |
| Package cache cap (MB) | min(1% of partition, 100 MB) |
| freepbx.log overflow cap per file (MB) | min(0.5% of partition, 20 MB) |
| Recording max age (days) | 90 |
| Asterisk log max age (days) | 30 |
| FastAGI log max age (days) | 30 |
| fail2ban log max age (days) | 30 (also the hard floor) |
| queue_log max age (days) | 60 |
| FreePBX backups to keep | 3 |
| Intruder-log preserve floor (months) | 1 = 30 days (editable; 0 = never delete) |
| Action threshold (% used) | 80 |
| Emergency threshold (% used) | 90 |
Every value is editable from the admin UI; defaults are not overwritten on subsequent upgrades.
A three-tab admin UI: Status (the visualization), Thresholds (the retention knobs), and Offsite (transport & per-category archive policy).
Each wedge ≥ 4% gets its own label; the legend shows values for every category including sub-4%. Run Dry-Run Now and Run Now buttons spawn the janitor on demand and stream the result back into the page.
Ship to remote storage before local deletion. Five categories can opt in: Recordings, FreePBX backups, fail2ban logs, Asterisk rotated logs, queue_log.
Delete in place per the age knob (no offsite). Default for all except recordings.
Bundle into a zstd tarball, ship via the configured transport, only delete locally on successful upload. If shipping fails, keep the files and retry. Default for recordings.
Try once, delete regardless of outcome. Best-effort archive for low-value-but-nice-to-have data.
Configured once in the Offsite tab; the same transport is reused by every category that opts in. SSH key is pasted as PEM into a textarea; the module saves it to /var/lib/asterisk/customdisk/offsite_ssh_key at chmod 600 and normalizes CRLF→LF (a common browser-textarea pitfall that breaks OpenSSH).
After max_retry consecutive failed ship attempts (default 3), choose:
Both emails are rate-limited (1 h emergency, 24 h action-threshold) so a sustained condition doesn't flood your inbox.
A Test transport button ships a 1-byte file end-to-end so you can validate before going live.
Every retention knob in one form. Safe ranges enforced server-side (no setting fail2ban below 30 days, etc.).
After the nightly janitor finishes, if disk is still at or above this percent, email goes out (rate-limit: 1 per 24 h). Same notification fires from the hourly health check if usage crossed the line between nightly runs.
The hourly check spawns an out-of-band cleanup pass (rate-limit: 1 per 4 h to prevent runaway loops), sends an urgent email (rate-limit: 1 per hour, separate anchor so emergency never gets swallowed by the 24 h limit).
Notification email auto-populated from FreePBX's Module Admin → Notification email setting if not overridden locally; subject line uses the System Identifier (FREEPBX_SYSTEM_IDENT).
Hard exclusions baked into every category function. The module manages data; it doesn't own it.
/etc/ — neverSystem configuration is off-limits, full stop.
*.sqlite3, *.db, *.ibd files are excluded. Any file owned by mysql is excluded.
/var/lib/asterisk/sounds, /moh, and /keys are never touched.
CDR retention is a separate concern. See the Custom CDR module for read-only reporting; CDR retention is handled by cdrpro or Custom CDR v1.1.
Voicemail only age-outs if the admin explicitly enables it with a non-zero voicemail.max_age_days.
If /var/lib/asterisk/backup.lock is present, the entire janitor run aborts cleanly.
OAuth2-protected endpoints under /api/customdisk/ — built for Zabbix, Nagios, PRTG, and your own dashboards.
| Endpoint | Purpose |
|---|---|
GET /status | Current disk usage + per-category reclaimable + last-run summary + config snapshot |
POST /run | Spawn a manual cleanup run, returns {run_id} |
POST /dry-run | Same but read-only estimate |
GET /run/{id} | Poll for completion + per-category JSON breakdown |
Cron worker runs as asterisk. Privileged operations get narrow sudoers entries written at install time:
Four lines, every command pinned exactly. visudo -c validates before atomic rename — a typo can't lock you out of sudo.
Two MySQL tables: customdisk_config (KV settings) and customdisk_runs (history of every cleanup pass with per-category JSON breakdown). Zero schema dependencies on Asterisk's data.
Cron + flock watchdog: missed runs auto-recover within 60 seconds; flock -n prevents overlap; never pgrep -f (which races on the shell process itself).
Standard FreePBX module. First install (running as root) writes the sudoers file and creates lock/log files; subsequent web-UI installs reuse them.
Cron entries auto-register as the asterisk user. No reload of Asterisk is needed for the module itself to start working — the nightly cron picks it up on the next 03:00.
In FreePBX, go to Admin → Module Admin → Upload modules, upload the tarball, then click Install and Apply Config.
Open Admin → Custom Disk → Status → Run Dry-Run Now. The dry-run reports bytes-that-would-be-freed per category without writing anything. Sanity-check the numbers, then click Run Now if they look right.
Only if you let it. The default is to delete recordings older than 90 days only after they're successfully shipped offsite. If no offsite transport is configured and the recordings age policy is "ship first, keep on failure" (the default), nothing gets deleted locally until the upload succeeds. Set the recording max-age knob to 0 and the category never runs.
No. CDR pruning is a completely separate concern and out of scope. The module never touches any MySQL file, anything owned by mysql, or any *.sqlite3/*.db/*.ibd file. See the Custom CDR module for CDR reporting; CDR retention itself is handled by cdrpro or Custom CDR v1.1.
The janitor checks for /var/lib/asterisk/backup.lock before doing anything. If a backup is in progress, the entire run aborts cleanly — it never competes with a backup write.
Yes — that's the recommended first step. The Status tab has Run Dry-Run Now which reports bytes-that-would-be-freed per category without writing anything. Sanity-check the numbers, then click Run Now if they look right.
Hard floor of 30 days, server-side, regardless of what you set in the UI. The module will never delete asterisk/full*, auth.log*, secure*, syslog*, or fail2ban-* within that window. You can set the floor to 0 (never delete) if you want to keep intruder logs forever.
Yes — MIT licensed. No license keys, no subscriptions, no phone-home. Built on FreePBX's BMO framework.
fwconsole ma uninstall customdisk drops the two MySQL tables, removes the cron entries, removes the sudoers file. Asterisk logs, recordings, and astdb are untouched — the module manages data; it doesn't own it.
Free up roughly 3 GB per PBX on the first run, then keep the partition healthy automatically. No 3 a.m. calls.