Profile
Back to NewsBack
GitHub Trending 34 min
Reader Mode
mosamlife/wpmgr: Open-source, self-hostable WordPress fleet management: backups, updates, uptime monitoring, security, and image optimization for every site from one dashboard you own. A self-hosted MainWP and WP Remote alternative.

mosamlife/wpmgr: Open-source, self-hostable WordPress fleet management: backups, updates, uptime monitoring, security, and image optimization for every site from one dashboard you own. A self-hosted MainWP and WP Remote alternative.

19 hours ago

WPMgr

Open-source, self-hostable WordPress fleet management.

WPMgr lets you enroll, monitor, update, back up, and secure a fleet of WordPress sites from one dashboard, running on infrastructure you control when you self-host it. The control plane is a Go binary with a React dashboard; a lightweight PHP plugin on each managed site handles the work. Everything between the agent and the control plane is Ed25519-signed.

Latest release</a> License</a> CI</a> WordPress.org</a>

WPMgr: run your whole WordPress fleet from one dashboard you own</a>

Website · Live dashboard · API reference

v0.61.163: open-source and production-usable for self-hosters. The agent plugin is reviewed and listed in the WordPress.org plugin directory.


Quick start

Get the whole stack running on your own server with one command, no repo clone needed:

curl -fsSL https://raw.githubusercontent.com/mosamlife/wpmgr/main/scripts/quickstart-selfhost.sh | bash

The script downloads every file the stack needs, generates all secrets, and prints the exact command to bring WPMgr up. You need a 64-bit Linux host with Docker 24+ (2 GB RAM is enough to start). See system requirements for sizing by fleet size, or the full quickstart below for options, prebuilt images, and the build-from-source path.


Features

Fleet connection

  • Live enrollment: Add a site by URL; paste a one-time code into the agent plugin; the dashboard flips from "Awaiting" to "Connected" automatically, no refresh needed.
  • Real-time connection state machine: Six precise states (pending_enrollment → connected → degraded → disconnected → revoked → archived) replace a vague up/down flag. Every transition is written to auditable, hash-chained history.
  • 60 s heartbeat with auto-recovery: A background sweeper degrades → disconnects silent sites within minutes. A returning agent auto-recovers without operator action.
  • Fleet SSE stream: One shared Server-Sent Events stream keeps the entire sites list live (status dots, last-seen counters) without polling. Cursor-based replay catches events missed while offline; all performance and scan surfaces refresh automatically on reconnect.
  • One-click autologin to wp-admin: Single-use, short-lived, audited EdDSA tokens; no shared passwords. Deep-links straight to Plugins or Themes, or log in as a specific user. Bypasses common two-factor plugins.
  • Signed dashboard-to-agent revoke: Revoke from the dashboard; the agent verifies a signed token on its next heartbeat and self-destructs. A man-in-the-middle on the heartbeat response cannot forge the teardown.
  • Signed last-will on deactivate/uninstall: Agent disconnects itself on deactivation (3 s best-effort); the timeout sweeper is the safety net if it never arrives.
  • Re-enrollment under a stable identity: Re-connecting a site keeps the same site_id, preserving all backup history, scan runs, and lifecycle generations.
  • Archive / restore soft-delete: Retire sites from the active view without losing their history; restore them later.
  • Per-site sharing: Share exactly one site with a collaborator, enforced by both Gin middleware and Postgres RESTRICTIVE RLS, without exposing the rest of the fleet.
  • Agent self-update channel: Self-hosted control planes push signed agent releases to enrolled sites. The WordPress.org build ships without the self-updater and updates from the site's own Plugins screen instead.

Backups & restore

  • Pure-PHP streaming DB dump: Server-side cursor (MYSQLI_USE_RESULT), REPEATABLE READ snapshot, ~1 MiB batched INSERTs. No mysqldump binary, no shell access, so it works on locked-down managed hosts. Memory use is independent of database size.
  • Pure-PHP streaming file archiver: ZipArchive streaming (never loads file bodies into memory), rotated at 200 MiB / 55 k entries per part. Splits wp-content into per-component sequences (plugins, themes, uploads, other, WP core) for targeted restore.
  • Incremental archive-delta backups: Each increment diffs the live file tree against the parent snapshot's files.list by size + mtime and packs only changed or new files into standard part archives, with deletions recorded as tombstone manifest sidecars. The database is dumped in full every run.
  • Selective-component backups + exclusion patterns: Per-site choice of which components to archive (plugins / themes / uploads / wp-content / database / WP core) plus exclude-path, exclude-extension, and max-file-size filters pushed to the agent on every run. Backup content settings are decoupled from the schedule so manual and scheduled runs share one definition.
  • Content-addressed chunking with dedup: Each artifact is chunked at ~4 MiB, content-hashed with BLAKE2b-256, and deduplicated across snapshots. Only changed chunks re-upload on the next full backup.
  • Three backup destinations: Control-plane-managed bucket (the default, so unless a site is pointed elsewhere the control plane holds its chunks), customer-owned S3-compatible bucket (agent never holds the credentials), or a local folder on the WordPress host. Chunks are stored as uploaded; uploads to a managed or S3-compatible bucket go over HTTPS, and a local destination writes straight to disk on the site's own server with no network transfer. Nothing is encrypted client-side in shipped builds, so whichever destination you pick, that destination's own access controls are what protect the data, along with whatever encryption at rest it provides: a bucket can be configured for it, a local folder gets only whatever the host's disk does. See docs/features/backups.md.
  • SQL inspection at backup time: A streaming constant-memory scanner produces sql-inspection.json (charset, table prefix, per-table row/byte estimates, WordPress detection, siteurl/home/db_version) stored with every snapshot.
  • Environment fingerprint: environment.json captures PHP/MySQL/WordPress versions, plugin/theme slugs, table list, and size at snapshot time.
  • Resumable watchdog-driven state machine: Phases are checkpointed to a task row; a watchdog re-enters stalled backups up to 6 times. Long backups survive PHP time limits and FPM worker recycling without redoing finished work.
  • Scheduled backups: Hourly, every-N-hours, daily, weekly, or monthly cadences, run in the site's own WordPress timezone, jittered per site so the fleet doesn't fire simultaneously.
  • Snapshot lock: Lock a snapshot to exempt it from retention GC regardless of age or keep-last rules; unlock explicitly when no longer needed.
  • Mark-and-sweep retention GC: Tenant-global reachability walk deletes a chunk only when it is unreachable from every retained snapshot across all sites and predates a fail-closed grace floor. Refcount is observability-only and never consulted for delete decisions.
  • Backup-completion email: Per-site notification config (always / on-failure / never + recipient list) sends a branded email on backup completion or failure for both manual and scheduled runs.
  • Live SSE progress: Phase-by-phase progress including chunk and byte counters streams to the dashboard in real time.
  • Point-in-time chain restore with version picker: Restoring a chain member overlays each generation's parts in order (newest-wins extraction) then applies tombstone deletes, so any snapshot in the chain restores correctly. The restore dialog renders a version picker across all completed chain generations.
  • Component-scoped restore: Restore the whole site, just the database, just files, or fine-grained components (plugins, themes, uploads, wp-content). Compose with per-path or per-table lists.
  • Atomic file restore: Extract to a hidden staging directory, move live tree aside to a .wpmgr-old-files-/ rollback dir, rename staging into place. A crash never leaves a half-merged site.
  • Online DB restore: Import into tmp_-prefixed tables while WordPress stays live; swap each table atomically via DROP + RENAME at the end. A failed import leaves the live database untouched.
  • Resumable restore: Same watchdog pattern as backup: persisted phase state, chunk-level download resume, mid-table URL-rewrite resume.
  • Two-leg disk-free precheck: Estimates required disk before touching anything; refuses with a GB-denominated message if there is not enough space.
  • Self-preservation guards: Never clobbers the running agent plugin, keystore, wp-config.php, .htaccess, or cache drop-ins; copies them forward from the live tree into staging before the swap.
  • Path-traversal & integrity hardening: Every downloaded chunk is verified against its content hash; every zip entry is traversal-checked with a canonical-path containment check against the staging root.
  • Maintenance-mode windowing: Drops WordPress's .maintenance file around the destructive swap; removes it and flushes object cache, OPcache, and rewrite rules on completion.

Updates

  • Per-site available-updates inventory: WordPress core, plugin, and theme update lists with current → new versions, active-first sorted.
  • On-demand inventory refresh: Force re-poll WP update transients via a signed CP→agent command; returns 409 with a clear message when a site is unreachable.
  • Bulk fleet-wide update runs: Target an explicit set of site IDs or a tag; expands to per-(site, item) tasks.
  • Dry-run preview: The bulk wizard defaults to dry_run=true so the first submit is a safe preview with exact version deltas.
  • Pre-update snapshot + automatic health-check rollback: The agent snapshots the component before updating; the CP health-probes the site after; a bad update is reverted automatically.
  • WP-CLI-first with PHP upgrader fallback: Works with or without WP-CLI; preserves active-plugin state on the PHP fallback path.
  • Live SSE progress per run: Task status transitions stream in real time; a snapshot-on-connect plus a 2 s poll safety net prevent stale "Queued" states.
  • Post-update inventory auto-refresh: Debounced per-site (30 s window) after every terminal task so the available-updates list self-heals.
  • Argument-injection hardening: Slugs and versions are validated on both the CP (validateItems) and agent (sanitizeSlug, isValidVersion) against a safe charset; shell metacharacters, .. traversal, and flag separators are rejected.
  • Per-tenant concurrency isolation: Sharded River queues with a per-tenant running-task limit; one large fleet update cannot starve other tenants.
  • Update run history: Auditable trail of who updated what, when, outcome per site/item, with from/to version and timings.

Monitoring & health

  • Sites grid view with screenshots: A list/grid toggle on the Sites dashboard. Each grid card is led by a real screenshot captured server-side (headless Chromium in the media-encoder, SSRF-guarded) and refreshed on connect, weekly, and on demand. Cards show capability status (page cache, object cache, HTTPS, backups, multisite), version/host/client/tag metadata, uptime, latency, SSL expiry, and backup health. The Status and Tags filters compose with search and persist in the URL. See docs/features/sites.md.
  • Active uptime monitoring: SSRF-hardened probes classify up/down with a full timing breakdown: DNS, connect, TLS handshake, TTFB, total. Also detects a WordPress fatal-error or database-connection page served with HTTP 200 and reclassifies it as down. See docs/features/monitoring.md.
  • Uptime % + latency charts: Windowed reports over 7 d / 30 d / 90 d; downsampled server-side. Tenant-wide status summary across the whole fleet.
  • TLS certificate tracking: Expiry, issuer, and subject captured on every HTTPS probe; flags "renew soon" at < 14 days.
  • Pluggable time-series store: Postgres (default, one row per probe) or ClickHouse (MergeTree, 90-day TTL) selectable at boot.
  • Downtime/recovery alerts: Transition-only (opens incident on N consecutive failures, closes on recovery). One downtime alert and one recovery alert per outage, never a flood.
  • Alert channels: Email (SMTP) and HMAC-SHA256-signed webhook. Both uptime and high-severity security events share one channel config.
  • Full WordPress Site Health collection: Ships the complete WP_Debug_Data::debug_data() dump (all native sections plus third-party plugin contributions from Yoast, WooCommerce, ACF, etc.) centrally.
  • 14-category extended diagnostics with fault isolation: Identity, PHP, MySQL, filesystem, HTTP loopback, cron, themes, plugins, users, security constants, HTTPS, mail, performance, hosting, each in an isolated wrapper so one failing probe doesn't blank the screen.
  • Leapfrog diagnostic signals: Max-overdue WP-Cron age, OPcache hit-rate/memory, site_as_of_hash fingerprint (changes when any managed component moves), agent-side managed-host detection (Pressable, GridPane, WP Engine, Atomic/WPCOM, Kinsta, Flywheel, RunCloud, Cloudways), and control-plane offline egress-IP inference of the underlying cloud/IaaS provider (DigitalOcean, Hetzner, AWS, Google Cloud, Azure, Vultr, OVH) via embedded DB-IP ASN lookup (no API key; no IP leaves the operator). IP data by DB-IP.com.
  • JIT directory sizes: Fresh or cached (< 6 h), computed via du or PHP fallback, annotated with method + computed-at timestamp. Never blank like the native Site Health screen.
  • Privacy redaction: Agent-side recursive walker redacts admin_email, _password, _secret, *_token, API keys, and auth salts before the diagnostics blob leaves the site.
  • On-demand diagnostics refresh: One click re-collects all categories and lands the data before the response returns.
  • PHP error monitoring: Error + shutdown handlers (including a must-use plugin that arms before other plugins boot, catching bootstrap-time fatals). Deduped by md5(code:file:line:message) with occurrence counters and gzip-compressed backtraces. Near-immediate ship on fatal.
  • Per-site error config: Level bitmask + fingerprint silence list pushed to the agent. Silencing a fingerprint deletes its existing rows immediately. Fatals always captured regardless of mask.
  • Hash-chained activity log: ~30 WordPress events (posts, comments, users, auth + failed logins, plugins, themes, core updates, terms, security-relevant options, WooCommerce order/product events) written as a SHA-256 chain. Control plane re-verifies byte-for-byte on ingest and on demand.
  • Filtered, paginated activity feed: Newest-first cursor pagination with filters by event type, object type, actor, severity, and time range. Per-row chain_valid flag; a /verify endpoint reports the first broken link if any row was altered.

Security

  • Dashboard two-factor authentication: Protect your account with a TOTP authenticator app and/or a WebAuthn passkey or security key. A guided setup wizard walks through QR scan (or manual key entry), live-code confirmation, and saving 10 one-time recovery codes. A second-step challenge appears at login with an optional "remember this device for 30 days" (revocable per device). The Settings → Security screen manages authenticator apps, passkeys, recovery codes, and trusted devices. TOTP secret is age-encrypted at rest; recovery codes are argon2id-hashed and single-use; cloned-authenticator detection (WebAuthn sign-count); brute-force lockout; re-auth required to disable; all events in the audit log. Every session-issuing path (password login, SSO, email verify) gates through one function, so no parallel path can bypass it. Self-hosted operators must set WPMGR_AUTH_WEBAUTHN_RPID and WPMGR_AUTH_WEBAUTHN_RP_ORIGINS for passkeys to work on their own domain; the authenticator-app factor works on any origin. See docs/features/2fa.md.
  • Core file-integrity scan: Resumable, cursor-paginated MD5 walk diffed against the official WordPress.org Checksums API for the site's exact version + locale.
  • Three finding types: core_modified (high), core_missing (medium), core_unknown_injected (high). Only flags within core paths; operator-mutated files (wp-content/, wp-config.php, cache drop-ins, etc.) are allow-listed to minimize false positives.
  • Checksum caching: 30-day positive TTL (releases are immutable), 6-hour negative TTL, transparent en_US locale fallback. Repeated fleet scans never hammer the public wp.org API.
  • Finding triage: Mark findings ignored (audited). Fetch raw file contents in-dashboard (server-gated: only stored findings, access audited) without SSH.
  • Brute-force login protection: Three escalating tiers per sliding window: captcha gate → per-IP temporary block → global site-wide block. Known-good bypass (recent success from same IP) avoids locking out legitimate admins.
  • Three protection modes: disabled (inert by default), audit (records/logs, never blocks), protect (enforces 403). Malformed config push falls back to safe defaults.
  • Early-boot WAF IP firewall: Must-use plugin (loads before any other plugin) checks deny CIDRs at the very start of WordPress boot; allow CIDRs and private/loopback IPs always win first. DB error fails open.
  • Operator-controlled CIDR allow/deny: IPv4 + IPv6, validated on the CP (net.ParseCIDR), binary inet_pton bitmask comparison on the agent. Spoof-resistant configurable real-IP header.
  • Lockout safety rail: Enabling protect mode with an empty allow-list auto-adds the operator's own IP as a /32 or /128.
  • Manual IP unblock: Deletes the IP's failure rows, resetting its sliding-window counter while preserving success rows.
  • Login-page whitelabeling: Per-site logo URL, logo link, and message. URLs scheme-validated (http/https only); message run through a narrow wp_kses allowlist (no script/style/iframe/on*). Safe to inject CP-controlled content into wp-login.php.
  • Hashed, role-scoped API keys: wpmgr__ format; only sha256 hash + prefix stored; secret shown once at creation; constant-time compare (crypto/subtle); per-key role flows through the full RBAC matrix.
  • Per-site access enforcement: Every scan, login-protection, login-brand, and unblock route requires RequireSiteAccess. Routes resolved by global ID (get-run, ignore-finding, fetch-file) re-resolve the object's real site and call CanAccessSite before reading or mutating.
  • Site-user two-factor, password policy, hide-login, and hardening toggles, plus a vulnerability scanner: per-site controls covering the WordPress site's own users (not the dashboard operator). See docs/features/security.md.

Performance

  • Zero-DB page cache fast path: An advanced-cache.php drop-in serves anonymous pages as pre-gzipped HTML straight from disk (wp-content/cache/wpmgr) before WordPress, plugins, or the theme load. A cache HIT makes zero DB or plugin calls and streams the .html.gz with a 304 Not-Modified short-circuit. Every cached response carries an x-wpmgr-cache header.
  • Variant-aware cache key: Pages cache separately per device (mobile UA), per logged-in role, per operator-included cookie, and per cache-varying query string (marketing params stripped). The drop-in's key algorithm is byte-identical to the PHP writer.
  • Safe cacheability gates: Never caches non-200, admin/login/AJAX/feed/sitemap paths, password-protected posts, or any request carrying a bypass cookie (WooCommerce/EDD cart + session, logged-in, comment-author). WooCommerce cart/checkout/account pages are excluded outright.
  • WooCommerce cart-session shell caching: Catalog pages (shop, category, home, blog) can be cached for shoppers who already have items in their cart; cart totals update live via WooCommerce's own cart-fragments mechanism. WPMgr auto-probes theme support across real storefront renders before surfacing the toggle, requiring three independent page confirmations before reporting support.
  • Automatic server fast-path install: Enabling the cache writes the WP_CACHE define, installs the drop-in, and adds an Apache .htaccess block that serves .gz without PHP. nginx gets a copy-paste try_files snippet; built-in handling for OpenLiteSpeed and WP Engine Atomic.
  • Purge: all, per-URL, and auto on content change: Manual purge of the whole cache or a single URL's variants, plus automatic invalidation on post/comment/product/template change that purges and re-warms the exact affected URL set (permalink, home, archives, every assigned + ancestor term).
  • Host / edge-cache purge integrations: Every purge fires wpmgr_purge_*:before/:after actions so managed-host and CDN edge caches (Varnish, Kinsta, WP Engine, etc.) clear in lockstep.
  • Full-site preload warmer: Warms every cacheable URL (home, all public post types including WooCommerce products, every non-empty term archive, author archives) across desktop and mobile UAs, via a custom self-dispatching MySQL queue with atomic claim/lock, retry/backoff, and same-host SSRF filtering.
  • Operator-tunable preload throttle: Concurrency (1-4 workers), inter-request delay (0-10000 ms), batch size (1-500), and a per-core load-average ceiling above which a pass defers, clamped on both control plane and agent.
  • Remove Unused CSS: own engine, no headless browser: The agent POSTs page HTML + CSS to the control plane, which computes used-CSS with its own engine (no third-party service), caches it in object storage, and dedups across pages of the same structure hash. The agent inlines the used-CSS and defers the originals. Any failure or cache-miss serves full CSS unchanged, so rendering is never blocked.
  • RUCSS safelist: Built-in safelist of slider/lightbox/runtime-state classes plus operator-defined selectors are force-kept so JS-driven widgets survive the purge.
  • Post-compute cache reheat: When the async RUCSS worker finishes, the control plane purges the URL and re-fetches it so the next render is a HIT with optimized HTML already cached.
  • CSS/JS minification: Local same-site stylesheets and scripts are minified, content-addressed into a local asset cache, and tag URLs rewritten. Already-minified and external files are skipped.
  • JavaScript delay (defer / interaction / idle): Defers first-party JS via the defer attribute, or rewrites scripts to data-src and runs them only on first interaction or requestIdleCallback. Structural scripts (ld+json, speculation rules) are never delayed.
  • Font optimization: font-display:swap injected into @font-face + Google Fonts links, optional self-hosting of Google Fonts stylesheets + woff2 files, and heuristic rel=preload for the first critical fonts.
  • WOFF2 font transcoding: Self-hosted TTF, OTF, and WOFF fonts are transcoded to WOFF2 by the media-encoder service (50 to 65% smaller for TTF/OTF, 20 to 30% for WOFF). The original serves until transcoding completes; any failure falls back to the original.
  • Glyph subsetting: When subsetting is enabled alongside WOFF2 transcoding, the encoder produces a latin-ext subset (U+0000 to 024F, U+1E00 to 1EFF) alongside the full WOFF2. Typical additional savings are 60 to 90% for body-text Latin fonts. Variable and icon fonts are detected and skipped. Per-font processing states (pending / converting / ready / subsetted / skipped / failed) are shown in a live table on the Optimize tab.
  • Third-party asset self-hosting: Downloads cross-origin CSS/JS to the local asset cache and rewrites tags to same-origin. Best-effort: failure leaves the external URL in place.
  • Image markup optimization (CLS + lazy-load): Fills missing width/height from getimagesize() or the filename WxH suffix to prevent layout shift. Adds loading=lazy + decoding=async to below-the-fold images while keeping the first two eager; existing srcset preserved.
  • YouTube facade + Gravatar self-host: Replaces YouTube iframes with a click-to-load thumbnail facade (no YouTube JS until clicked) and downloads/self-hosts Gravatars to the local asset cache.
  • Speculation Rules prefetch: Injects a Chat with me