feat: add internal WordPress cron runner

This commit is contained in:
bisco
2026-07-23 23:40:44 +02:00
parent f75bce3dea
commit e6ec3c94dc
15 changed files with 245 additions and 24 deletions
+11 -6
View File
@@ -17,10 +17,12 @@ classic theme for exact semantic markup and responsive design. Store homepage fi
sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
post types so structured content is not lost when changing themes.
NGINX is the only public entry point. WP-CLI provides an idempotent opt-in bootstrap;
Certbot provides opt-in direct TLS. Docker networks isolate the database. Runtime state
uses host-based bind mounts with a preparation script for ownership/mode, while
functional tests use separate Docker volumes.
NGINX is the only public entry point. Public `wp-cron.php` requests stay blocked, while
an internal WP-CLI based `wp-cron` service runs due scheduled events over the private
data network. WP-CLI provides an idempotent opt-in bootstrap; Certbot provides opt-in
direct TLS. Docker networks isolate the database. Runtime state uses host-based bind
mounts with a preparation script for ownership/mode, while functional tests use
separate Docker volumes.
## Consequences
@@ -30,6 +32,8 @@ functional tests use separate Docker volumes.
- Core/theme/plugin patches require an image rebuild in production.
- Fixed participation cards remain code until editorial requirements justify another
structured content type.
- Scheduled WordPress jobs depend on the internal `wp-cron` service instead of visitor
traffic.
## Alternatives considered
@@ -43,8 +47,9 @@ functional tests use separate Docker volumes.
The architecture inherits WordPress's public CMS attack surface. The implementation
reduces it with network isolation, no direct application port, disabled file editing
and XML-RPC, production immutability, sanitization/escaping, rate limiting, security
headers, and fail-closed TLS. Admin MFA and allowlisting remain external controls.
and XML-RPC, production immutability, sanitized/escaped rendering, no bundled demo
plugins, internal-only scheduled jobs, rate limiting, security headers, and fail-closed
TLS. Admin MFA and allowlisting remain external controls.
## Operational impact
+7 -4
View File
@@ -12,10 +12,13 @@ structured editorial content survives a theme change. These custom post types ar
editorial data sources for the homepage, not standalone public routes or REST
collections. Images use WordPress featured images with local SVG fallbacks.
WP-CLI is an opt-in tools-profile service. Its idempotent bootstrap installs WordPress,
activates the theme, configures the site, and creates realistic demo content. Certbot is
another optional service, enabled only for direct deployments. It shares challenge and
certificate volumes with NGINX but has no container-control access.
`wp-cron` is an internal WP-CLI runner on the `data` network. It executes due scheduled
events with `wp cron event run --due-now` and keeps public `wp-cron.php` requests
blocked. WP-CLI is also available as an opt-in tools-profile service. Its idempotent
bootstrap installs WordPress, activates the theme, configures the site, and creates
realistic demo content. Certbot is another optional service, enabled only for direct
deployments. It shares challenge and certificate volumes with NGINX but has no
container-control access.
Apache includes a small defense-in-depth hardening file that denies uploaded PHP files,
direct `wp-config.php` requests, and direct access to selected internal WordPress PHP
+5
View File
@@ -11,6 +11,8 @@ docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
NGINX binds to loopback ports 8080/8443. WordPress and MariaDB remain private. The
bootstrap is safe to rerun and does not duplicate demo records.
The internal `wp-cron` service runs scheduled WordPress events after the bootstrap
completes.
The default persistent paths are host-based bind mounts under `./runtime`. Before the
first start, run:
@@ -37,6 +39,9 @@ Required controls:
- either direct Let's Encrypt termination or a trusted external load balancer;
- off-host database/file backups and monitoring.
The default `wp-cron` service handles scheduled WordPress events internally and should
remain enabled unless another controlled cron runner replaces it.
When a load balancer terminates TLS, Certbot stays disabled. `TRUST_PROXY_HEADERS=1`
is safe only when firewall/network policy makes the load balancer the sole NGINX
caller and it overwrites `X-Forwarded-*` headers.
+9 -7
View File
@@ -5,9 +5,10 @@
```bash
docker compose up --build -d
docker compose ps
docker compose logs -f proxy wordpress db certbot
docker compose logs -f proxy wordpress wp-cron db certbot
./scripts/prepare-host-volumes.sh
docker compose --profile tools run --rm wp-cli -c 'wp core version'
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
docker compose down
```
@@ -37,10 +38,11 @@ docker compose restart wordpress proxy
Do not use the maintenance override for routine bootstrap or inspection commands that
do not need outbound network access.
External `wp-cron.php` requests are blocked to reduce public attack surface. If future
content scheduling, maintenance jobs, or plugin features require WordPress cron, run it
from a controlled host/container cron against the private WordPress service instead of
leaving the public trigger enabled.
External `wp-cron.php` requests are blocked to reduce public attack surface. The
default `wp-cron` service runs due scheduled events internally through WP-CLI every
`WP_CRON_INTERVAL_SECONDS` seconds, defaults to 300, and joins only the internal `data`
network. If a scheduled job is late, inspect `docker compose logs wp-cron wordpress db`
and run `docker compose --profile tools run --rm wp-cli -c 'wp cron event list'`.
## Backup and restore
@@ -61,8 +63,8 @@ the database and file volume together, restart, and verify the homepage, media,
owner or mode; run `./scripts/prepare-host-volumes.sh` after changing paths or image
user IDs.
- SMTP is not configured; WordPress password-reset email needs an external mail service.
- WordPress cron is not publicly triggerable; scheduled jobs need an operator-managed
cron runner if that feature becomes necessary.
- WordPress cron is not publicly triggerable; scheduled jobs depend on the internal
`wp-cron` runner being healthy.
- Admin MFA and network allowlisting are deployment concerns and are not bundled.
- WordPress plugins expand the attack surface; install only reviewed, maintained,
necessary plugins.
+33
View File
@@ -50,6 +50,39 @@ docker compose restart wordpress proxy
If DNS still fails with the maintenance override, inspect the host/container DNS
configuration and firewall rules before retrying the update.
## Scheduled WordPress events are late
The public `/wp-cron.php` trigger is intentionally blocked. Scheduled jobs are executed
by the internal `wp-cron` service.
1. Confirm the runner is present:
```bash
docker compose ps wp-cron
```
2. Inspect due events:
```bash
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
```
3. Inspect runner logs without printing `.env` values:
```bash
docker compose logs --tail=100 wp-cron wordpress db
```
4. If needed, run due events manually:
```bash
docker compose --profile tools run --rm wp-cli -c 'wp cron event run --due-now'
```
The Site Health loopback test may still report a blocked public loopback while the edge
continues to deny `/wp-cron.php`; that is an expected trade-off of the hardened public
configuration.
## Uploaded image does not appear on the public page
1. Confirm the image was selected in **Appearance > Customize**, not only uploaded in
+5 -1
View File
@@ -11,6 +11,8 @@
collections.
- File editing is always disabled. Production also disables web-based core, theme, and
plugin changes; patched images are rebuilt and redeployed instead.
- The WordPress image removes the bundled Akismet and Hello Dolly plugins, and the
entrypoint also removes them from existing persistent volumes on startup.
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
direct `wp-config.php` requests, the WordPress readme/license files, direct
installation entry points, public mu-plugin directory probing, and external
@@ -28,7 +30,9 @@
WordPress/Apache retains the capabilities needed by the official image internally,
but no WordPress port is published. A containerized security test fails if the
WordPress service is configured with host-published ports.
- WP-CLI normally joins only the internal data network. The maintenance override
- `wp-cron` runs WordPress scheduled events through WP-CLI on the internal data network
only; public `wp-cron.php` remains blocked. WP-CLI normally joins only the internal
data network. The maintenance override
attaches WP-CLI to the web network for operator-triggered commands that require
outbound internet access, such as WordPress core downloads; do not use it for routine
bootstrap or inspection commands.
+2 -1
View File
@@ -23,7 +23,8 @@ cover content and section order, contact actions, mobile overflow/navigation, se
landmarks, image alternatives, admin routing, security headers, blocked sensitive
routes, blocked uploaded PHP requests, and unknown virtual hosts. Security checks also
assert that WordPress does not publish host ports and that Apache hardening remains
installed in the WordPress image.
installed in the WordPress image. They also assert that the default demo plugins are
removed and that the internal `wp-cron` service has no public network exposure.
Subjective visual review and a real-device accessibility audit remain manual release
checks.