wp-updater
Self-hosted dashboard to scan WordPress sites for core/plugin/theme updates and apply them.
1.4K
A small, self-hosted, MainWP-style dashboard that scans the WordPress sites you run and reports available core, plugin, and theme updates — with a simple web GUI, scheduled scans, email/Telegram reports, per-site auto-update toggles, optional one-click "update all", WPScan vulnerability flagging, post-update health checks, pending-update age tracking, and an optional weekly digest.
No SaaS, no per-site fees, no SSH into the sites required — the dashboard talks to each site over HTTPS using a per-site API key.
It has two parts:
| Part | What it is | Where it runs |
|---|---|---|
| Connector | A single-file mu-plugin exposing a secret-protected REST endpoint | On each WordPress site |
| Dashboard | A Flask app (Docker container) that polls each site and shows the GUI | On your own server / Docker host |
Heads up: the update / auto-update endpoints perform privileged actions on your sites. Run the dashboard on a trusted network and put it behind HTTP basic auth or a reverse proxy (see Securing the dashboard).
The published image builds the React SPA and serves it together with the JSON API from one container — no Node or build step needed on your side.
mkdir wp-updater && cd wp-updater
# 1. Create a data directory the container (uid 10001) can write to.
mkdir -p data && sudo chown -R 10001:10001 data
# 2. Run it.
docker run -d --name wp-updater \
-p 8090:8090 \
-e TZ=UTC \
-e WPUPDATER_SECRET_KEY="$(openssl rand -hex 32)" \
-v "$PWD/data:/data" \
--restart unless-stopped \
mfrankovic/wp-updater:latest
Open http://YOUR_HOST:8090.
Image: mfrankovic/wp-updater
(multi-arch: linux/amd64, linux/arm64).
# compose.yaml
name: wp-updater
services:
wp-updater:
image: mfrankovic/wp-updater:latest
container_name: wp-updater
restart: unless-stopped
ports:
- "8090:8090"
environment:
TZ: UTC
WPUPDATER_SECRET_KEY: change-me-to-a-long-random-string
# Optional HTTP basic auth (leave blank to disable):
WPUPDATER_USER: ""
WPUPDATER_PASSWORD: ""
volumes:
- ./data:/data
mkdir -p data && sudo chown -R 10001:10001 data
docker compose up -d
Open Settings → Application updates to see the installed dashboard version. Update checks are manual unless you opt in. When enabled, the browser checks once as the dashboard opens and every 24 hours while it remains open; the preference stays in the current browser.
The dashboard compares both its own version and each monitored site's reported MU connector version with the latest stable GitHub Release. A compact banner appears throughout the dashboard when either update is available. Connector notices identify affected sites and link to the release asset. Checks never download an image, change WordPress files, access Docker, or execute commands.
For a Compose deployment using mfrankovic/wp-updater, run:
docker compose pull wp-updater
docker compose up -d --no-deps wp-updater
For a local source build such as the included compose.yaml, run:
git pull
docker compose up -d --build wp-updater
The Settings card displays both command sets with copy buttons when a newer dashboard release exists. Connector updates remain manual: download the release asset and replace the MU plugin on the sites named in the notice.
wp-updater-connector.php from the
Releases page.wp-content/mu-plugins/ folder (create the
mu-plugins folder if it does not exist). Must-use plugins are always
active — no activation needed.For maximum safety you can instead hard-code the key in
wp-config.php:define('WPUPDATER_API_KEY', 'your-long-random-key');The Settings page will then show that fixed key.
The connector exposes (all require the X-WPUpdater-Key header):
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /wp-json/wpupdater/v1/status | core/plugin/theme versions + available updates + auto-update state |
| POST | /wp-json/wpupdater/v1/auto-updates (enable=true/false) | turn WordPress auto-updates on/off for all plugins & themes |
| POST | /wp-json/wpupdater/v1/update (targets=core,plugins,themes) | apply pending updates |
| GET | /wp-json/wpupdater/v1/ping | health/auth check |
Requires WordPress with a writable filesystem and PHP 7.4+.
In the dashboard click + Add site, then enter each site's Name, URL, API key (from step 1) and an optional Group. The site is scanned immediately so its row populates right away. To stop monitoring a site, use the Remove action — the trash icon in the Sites table row, or the Remove button in the site details drawer. Removing a site deletes its stored scan history in WP Updater only; it does not touch the WordPress site itself.
A built-in Help page (in the left sidebar) walks through connecting sites, scanning, deploying updates and editing a site.
Everything is configured with environment variables. The scan schedule, SMTP / email reporting and Telegram notifications can also be configured live in the Settings page (the environment variables below just seed the initial values).
The Settings → Schedule section offers an advanced scheduler — pick
Hourly, Daily, Several times a day, Weekly, Monthly, or a
Custom cron expression. Internally the schedule is stored as a standard
5-field cron string (scan_cron setting); the legacy WPUPDATER_SCAN_HOUR /
_MINUTE values are only used as the initial daily default, so upgrading an
existing install keeps the previous 06:00 daily scan until you change it.
Running a scan more than once a day is the recommended way to catch updates
that are published partway through the day.
| Variable | Default | Notes |
|---|---|---|
TZ | UTC | Local timezone for the scheduler (e.g. Europe/Zagreb) |
WPUPDATER_PORT | 8090 | Port inside the container (map it to any host port) |
WPUPDATER_SECRET_KEY | — | Flask session key; set a long random value |
WPUPDATER_USER / WPUPDATER_PASSWORD | empty | Optional HTTP basic auth |
WPUPDATER_VERIFY_TLS | true | Set false only for self-signed site certs |
WPUPDATER_REQUEST_TIMEOUT | 30 | Per-request timeout (seconds) |
WPUPDATER_SCAN_ENABLED | true | Enable the automatic scheduled scan |
WPUPDATER_SCAN_HOUR / _MINUTE | 6 / 0 | Initial daily scan time (local TZ); seeds the default cron. Change the cadence in Settings → Schedule |
SMTP_HOST … REPORT_RECIPIENTS | empty | Email reporting (see .env.example) |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID | empty | Telegram notifications (see .env.example) |
Data (SQLite DB + generated reports) is stored in the mounted /data volume and
survives container rebuilds.
The GUI has no login by default. Either:
WPUPDATER_USER / WPUPDATER_PASSWORD for HTTP basic auth (this also
protects the /api/* endpoints), orEach site uses its own 64-char API key sent in a request header over HTTPS; keep
WPUPDATER_VERIFY_TLS=true and rotate keys any time from the site's
Settings → WP Updater page.
9d), and the oldest pending age is tracked per site,
so long-ignored updates stand out./report.html and /report.md.The UI is deliberately scoped to updating WordPress core, plugins and themes only — no security scanning, uptime, backups, analytics, SEO, billing or client management.
git clone https://github.com/marinfrankovic/WP_Updater.git
cd WP_Updater
cp .env.example .env # set WPUPDATER_SECRET_KEY, TZ, optional auth + SMTP
mkdir -p data && sudo chown -R 10001:10001 data
docker compose up -d --build # multi-stage build compiles the SPA, no Node needed
compose.yaml builds the image locally from the Dockerfile. The first build
compiles the React SPA in a Node stage, then ships it inside the Python runtime.
cd frontend
npm install
npm run dev # http://127.0.0.1:5174 (proxies /api to http://127.0.0.1:8090)
npm run build # type-check (tsc -b) + build into ../app/webui
Set VITE_API_TARGET to point the dev proxy at a remote backend if needed.
Stack: React 19, TypeScript (strict), Vite, lucide-react icons, React Context +
useReducer, a single tokenised global stylesheet (src/styles/index.css).
Domain types in src/types/index.ts are the contract between the SPA and the
Flask API. Backend: Flask + gunicorn, stdlib SQLite, an in-process scheduler
thread, no external services.
Install the pinned development requirements and run backend tests:
python -m pip install -r requirements-dev.txt
python -m pytest
GitHub Actions runs the Python tests and the production frontend build on pushes to main and pull requests.
JSON API (consumed by the SPA, basic-auth protected like the GUI):
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/state | full state: sites, available updates, activity log |
| GET | /api/app-info | installed dashboard version and update commands; no external request |
| GET | /api/app-update | installed/latest stable release information and update commands |
| GET / POST | /api/schedule | read / change the scan schedule (cron) + on-off |
| POST | /api/sites (name,url,apiKey,group) | add a site + immediate scan |
| PATCH | /api/sites/<id> (name,url,group,apiKey) | edit site (blank apiKey keeps current) |
| DELETE | /api/sites/<id> | remove a site and its history |
| POST | /api/sites/<id>/scan | rescan one site |
| POST | /api/sites/<id>/auto-update (enabled) | toggle real WordPress auto-updates |
| POST | /api/sites/<id>/update (scope) | update core/plugin/theme/all |
| POST | /api/sites/<id>/update-item (type,slug) | update a single plugin/theme/core |
| POST | /api/scan-all | rescan every enabled site |
| POST | /api/bulk-update (siteIds,scope) | update many sites at once |
┌────────────┐ HTTPS + X-WPUpdater-Key ┌────────────────────┐
│ Dashboard │ ───────────────────────────▶ │ WP site connector │
│ (Flask) │ GET /status │ (mu-plugin) │
│ Docker │ ◀─────────────────────────── │ │
└────┬───────┘ JSON: versions + updates └────────────────────┘
│
├─ SQLite (sites, scans, settings)
├─ Scheduler thread → cron-scheduled scan + email/Telegram
└─ Reports (HTML / Markdown)
Pushing a vX.Y.Z tag triggers the Publish to Docker Hub GitHub Action, which
builds and pushes a multi-arch image to mfrankovic/wp-updater (:X.Y.Z,
:X.Y, :latest). Attach the matching wp-updater-connector.php to the GitHub
Release so the Help page link resolves to it.
Create a GitHub Release for every stable tag. The in-app checker reads GitHub's latest stable release endpoint, so pushing only a tag is not enough to announce an update.
Requires two repository secrets: DOCKERHUB_USERNAME and a DOCKERHUB_TOKEN
access token with Read/Write scope.
Content type
Image
Digest
sha256:3b870ce04…
Size
49.5 MB
Last updated
7 days ago
docker pull mfrankovic/wp-updater