Plone is one of the most battle-tested content management systems in the world — it powers government portals, university websites, and enterprise intranets that have been running continuously since the early 2000s. Built on the Zope application server with its unique ZODB object database, Plone's architecture is unlike any modern CMS: there's no SQL database, content lives as Python objects in a file-based database (Data.fs), and scaling is achieved through ZEO server clusters and Varnish caching rather than read replicas. When you self-host Plone, you're responsible for monitoring a stack that requires specific operational knowledge: ZODB packing schedules, blob storage growth, ZEO client connections, and Varnish cache efficiency. Vigilmon gives you comprehensive external monitoring across every layer of the Plone stack so your enterprise CMS stays available and healthy.
What You'll Set Up
- Plone application health monitor (HTTP uptime)
- ZODB Data.fs health and growth monitoring
- ZEO server connectivity check
- Blob storage disk usage alert
- Varnish cache hit rate monitoring
- Volto frontend (Plone 6) health check
- Python process memory leak detection via cron heartbeat
- ZODB pack schedule verification
- Alert channels with appropriate thresholds
Prerequisites
- Plone running via buildout, Docker, or pip install
- Plone accessible over HTTP (directly or via nginx/Apache reverse proxy)
- Varnish configured as a caching reverse proxy (recommended for production)
- ZEO server (if using clustered deployment)
- A free Vigilmon account
Step 1: Monitor Plone Application Health
The Plone/Zope process is the core of your CMS. Monitor it directly to catch crashes, failed migrations, and startup errors.
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - URL:
https://plone.yourdomain.com(orhttp://your-server-ip:8080/Plonefor direct Zope access). - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Keyword check, enter
Ploneto verify the CMS is actually serving content, not just that nginx responds. - Enable Monitor SSL certificate and set expiry alert to
21 days. - Click Save.
For a more targeted health check, monitor Plone's @@ok view which returns a simple 200 without rendering the full page:
https://plone.yourdomain.com/@@ok
This endpoint is lighter than a full page render and avoids caching complications.
Step 2: Monitor the Plone API Endpoint (Plone REST API)
Modern Plone deployments expose a REST API used by the Volto frontend and integrations. A healthy API response confirms Plone's database connectivity and request routing are working.
- Click Add Monitor → HTTP / HTTPS.
- URL:
https://plone.yourdomain.com/++api++/@site - Method:
GET - Add header:
Accept: application/json - Expected HTTP status:
200 - Under Keyword check, enter
"@id"— the Plone REST API response for the site root includes this JSON key. - Check interval:
2 minutes - Click Save.
A 500 on this endpoint with TCP up indicates a ZODB connectivity or middleware error.
Step 3: Monitor ZODB Data.fs Health
Plone's object database is stored in Data.fs. Monitor its accessibility and size to detect disk pressure and growth that signals the need for a pack operation.
Create a health sidecar script:
#!/usr/bin/env python3
# plone_probe.py — run as a systemd service or Docker sidecar alongside Plone
from flask import Flask, jsonify
import os
import time
app = Flask(__name__)
DATA_FS_PATH = "/var/lib/plone/var/filestorage/Data.fs" # adjust to your buildout path
BLOB_DIR = "/var/lib/plone/var/blobstorage"
DISK_WARN_PERCENT = 80
@app.route('/data-fs')
def data_fs():
try:
if not os.path.exists(DATA_FS_PATH):
return jsonify({"status": "missing", "path": DATA_FS_PATH}), 503
stat = os.stat(DATA_FS_PATH)
size_gb = stat.st_size / (1024 ** 3)
age_hours = (time.time() - stat.st_mtime) / 3600
# Check disk usage of the partition containing Data.fs
import shutil
disk = shutil.disk_usage(os.path.dirname(DATA_FS_PATH))
disk_percent = (disk.used / disk.total) * 100
status = "ok"
warnings = []
if disk_percent > DISK_WARN_PERCENT:
status = "disk_high"
warnings.append(f"Disk at {disk_percent:.1f}%")
if age_hours > 24:
warnings.append(f"Data.fs not written in {age_hours:.1f}h — Zope may not be persisting")
return jsonify({
"status": status,
"size_gb": round(size_gb, 2),
"disk_percent": round(disk_percent, 1),
"warnings": warnings
}), 200 if status == "ok" else 503
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 503
Add a Vigilmon monitor:
- Click Add Monitor → HTTP / HTTPS.
- URL:
http://your-server-ip:8090/data-fs - Check interval:
10 minutes - Expected HTTP status:
200
Step 4: Monitor ZEO Server Connectivity
If you're running a clustered Plone deployment with ZEO, the ZEO server is the single point of database access for all Plone instances. A ZEO server failure causes every Plone client to lose database access simultaneously.
- Click Add Monitor → TCP Port.
- Host: your ZEO server host (e.g.,
localhostor a dedicated ZEO server IP). - Port:
8100(default ZEO port). - Check interval:
1 minute - Click Save.
Add a secondary HTTP check that validates Plone clients are actually connected to ZEO (a TCP port being open doesn't mean Plone has successfully checked out a ZODB connection):
@app.route('/zeo-connectivity')
def zeo_connectivity():
import socket
ZEO_HOST = "localhost"
ZEO_PORT = 8100
try:
sock = socket.create_connection((ZEO_HOST, ZEO_PORT), timeout=5)
sock.close()
return jsonify({"status": "ok", "host": ZEO_HOST, "port": ZEO_PORT}), 200
except Exception as e:
return jsonify({"status": "unreachable", "error": str(e)}), 503
Step 5: Monitor Blob Storage Disk Usage
Plone stores uploaded files and images in a separate blob directory (blobstorage). Large file uploads accumulate quickly. Running out of blob storage space causes upload failures that appear as mysterious server errors to editors.
@app.route('/blob-storage')
def blob_storage():
try:
import shutil
usage = shutil.disk_usage(BLOB_DIR)
percent = (usage.used / usage.total) * 100
free_gb = usage.free / (1024 ** 3)
# Also count blob directory size
total_blob_gb = 0
for dirpath, dirnames, filenames in os.walk(BLOB_DIR):
for f in filenames:
try:
total_blob_gb += os.path.getsize(os.path.join(dirpath, f))
except OSError:
pass
total_blob_gb = total_blob_gb / (1024 ** 3)
if percent > DISK_WARN_PERCENT:
return jsonify({
"status": "high",
"disk_percent": round(percent, 1),
"free_gb": round(free_gb, 2),
"blob_size_gb": round(total_blob_gb, 2)
}), 503
return jsonify({
"status": "ok",
"disk_percent": round(percent, 1),
"free_gb": round(free_gb, 2),
"blob_size_gb": round(total_blob_gb, 2)
}), 200
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 503
Add a Vigilmon monitor:
- Click Add Monitor → HTTP / HTTPS.
- URL:
http://your-server-ip:8090/blob-storage - Check interval:
30 minutes - Expected HTTP status:
200
Step 6: Monitor Varnish Cache Hit Rate
Production Plone deployments rely heavily on Varnish to handle anonymous user traffic without hitting Zope for every request. A drop in cache hit rate means your Zope process is fielding traffic it shouldn't and will slow under load.
Varnish exposes statistics via varnishstat. Create a monitoring endpoint:
import subprocess
import json
VARNISH_HIT_RATE_MIN = 0.80 # 80% hit rate minimum
@app.route('/varnish')
def varnish():
try:
result = subprocess.run(
["varnishstat", "-1", "-j"],
capture_output=True, text=True, timeout=10
)
stats = json.loads(result.stdout)
cache_hits = stats.get("MAIN.cache_hit", {}).get("value", 0)
cache_misses = stats.get("MAIN.cache_miss", {}).get("value", 0)
total = cache_hits + cache_misses
if total == 0:
return jsonify({"status": "ok", "hit_rate": None, "note": "no traffic yet"}), 200
hit_rate = cache_hits / total
if hit_rate < VARNISH_HIT_RATE_MIN:
return jsonify({
"status": "low_hit_rate",
"hit_rate": round(hit_rate, 4),
"hits": cache_hits,
"misses": cache_misses
}), 503
return jsonify({
"status": "ok",
"hit_rate": round(hit_rate, 4),
"hits": cache_hits,
"misses": cache_misses
}), 200
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 503
Add a Vigilmon monitor with Check interval: 5 minutes.
Step 7: Monitor Volto Frontend Health (Plone 6)
If you're running Plone 6 with the Volto React frontend, the frontend is a separate Node.js service. Editors and public users on Plone 6 see Volto — if it goes down, the site appears broken even with Plone running perfectly.
- Click Add Monitor → HTTP / HTTPS.
- URL:
https://plone.yourdomain.com(Volto typically serves on the root, with the Plone REST API under/++api++). - Check interval:
1 minute - Expected HTTP status:
200 - Under Keyword check, enter
__NEXT_DATA__— the Next.js/Volto page includes this script tag in rendered HTML, confirming Volto rendered the page (not Plone Classic). - Click Save.
Optionally, add a direct check on the Volto Node.js process port if it's exposed separately:
- Click Add Monitor → TCP Port.
- Host:
localhost - Port:
3000(default Volto dev port; production is typically proxied through nginx)
Step 8: Track ZODB Pack Schedule via Heartbeat
ZODB's Data.fs accumulates old object revisions over time (every write creates a new revision, old ones persist until a pack). Without regular packing, Data.fs grows indefinitely. Monitor your pack schedule by pinging Vigilmon after each successful pack:
- Click Add Monitor → Cron Heartbeat.
- Set Expected interval to
720 hours(30 days — standard pack schedule for most Plone sites). - Copy the heartbeat URL:
https://vigilmon.online/heartbeat/abc123. - Add the heartbeat ping to your pack script or cron job:
#!/bin/bash
# /usr/local/bin/plone-pack.sh — run monthly via cron
set -e
# Run ZODB pack (adjust path to your buildout)
/srv/plone/bin/zodbpack /srv/plone/etc/zodb.conf --days=7
# Ping Vigilmon on success
curl -sf "https://vigilmon.online/heartbeat/abc123" || true
echo "Pack complete: $(date)"
Add to crontab:
0 2 1 * * /usr/local/bin/plone-pack.sh >> /var/log/plone-pack.log 2>&1
If no heartbeat arrives within 31 days, Vigilmon alerts that your pack schedule has missed — before Data.fs grows to fill your disk.
Step 9: Monitor Python Process Memory
Plone/Zope can develop memory leaks, particularly in long-running instances with many add-ons, large ZODB caches, or connection pool buildup. Monitor the Zope process RSS to catch gradual memory growth before it triggers the OOM killer.
import psutil
MEMORY_WARN_PERCENT = 90
@app.route('/memory')
def memory():
try:
zope_rss_mb = 0
zope_pid = None
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'memory_info']):
cmdline = ' '.join(proc.info.get('cmdline') or [])
if 'plone' in cmdline.lower() or 'zope' in cmdline.lower() or 'instance' in cmdline.lower():
rss = proc.info['memory_info'].rss / (1024 * 1024)
if rss > zope_rss_mb:
zope_rss_mb = rss
zope_pid = proc.info['pid']
vm = psutil.virtual_memory()
system_percent = vm.percent
status = "ok"
if system_percent > MEMORY_WARN_PERCENT:
status = "high"
return jsonify({
"status": status,
"zope_rss_mb": round(zope_rss_mb, 1),
"zope_pid": zope_pid,
"system_percent": round(system_percent, 1)
}), 200 if status == "ok" else 503
except Exception as e:
return jsonify({"status": "error", "error": str(e)}), 503
If you observe zope_rss_mb growing steadily over days, schedule a supervisorctl restart plone during a maintenance window before the OOM killer forces an uncontrolled restart.
Step 10: Configure Alerting
In Vigilmon, go to Alert Channels and configure your notification channels:
- Email: CMS team, IT operations
- Slack:
#plone-opsor#site-reliabilityvia webhook - PagerDuty: for government or enterprise deployments where availability is an SLA requirement
Recommended thresholds:
| Monitor | Recommended threshold | |---|---| | Plone HTTP health | Alert immediately on first failure | | Plone REST API | Alert after 2 consecutive failures (4 min) | | Data.fs disk usage | Alert at >80% | | ZEO server TCP | Alert immediately | | Blob storage disk | Alert at >80% | | Varnish hit rate | Alert when hit rate drops below 80% | | Volto frontend | Alert after 2 consecutive failures | | ZODB pack heartbeat | Alert if no pack in 31 days | | Python memory | Alert when system memory >90% |
Conclusion
A complete Vigilmon setup for Plone covers every layer of its unique enterprise architecture:
- Application health — catches Zope crashes and startup failures
- ZODB Data.fs — monitors file growth and disk pressure before corruption risk
- ZEO server — detects single-point-of-failure database access loss in clustered deployments
- Blob storage — prevents upload failures from silent disk exhaustion
- Varnish cache — detects cache invalidation issues that overload your Zope process
- Volto frontend — catches Node.js failures that break the Plone 6 user experience
- Pack schedule — enforces ZODB housekeeping before
Data.fsfills your disk - Memory monitoring — detects Zope memory leaks before the OOM killer strikes
With these monitors in place, your Plone CMS has the operational visibility it needs to run reliably for the years — and decades — your organization depends on it.
Get started with a free Vigilmon account.