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
+1
View File
@@ -11,6 +11,7 @@ WP_TITLE=Azione!Lab
WP_ADMIN_USER=azionelab-admin WP_ADMIN_USER=azionelab-admin
WP_ADMIN_PASSWORD=replace-with-a-local-admin-password WP_ADMIN_PASSWORD=replace-with-a-local-admin-password
WP_ADMIN_EMAIL=admin@example.org WP_ADMIN_EMAIL=admin@example.org
WP_CRON_INTERVAL_SECONDS=300
NGINX_BIND_ADDRESS=127.0.0.1 NGINX_BIND_ADDRESS=127.0.0.1
NGINX_HTTP_PORT=8080 NGINX_HTTP_PORT=8080
NGINX_HTTPS_PORT=8443 NGINX_HTTPS_PORT=8443
+11 -4
View File
@@ -2,8 +2,8 @@
A warm, editorial single-page website for the Azione!Lab contemporary theatre A warm, editorial single-page website for the Azione!Lab contemporary theatre
workshop. WordPress manages the content, a custom theme owns the visual system, and workshop. WordPress manages the content, a custom theme owns the visual system, and
Docker Compose provides MariaDB, NGINX, optional Let's Encrypt, WP-CLI, and isolated Docker Compose provides MariaDB, NGINX, an internal WordPress cron runner, optional
browser tests. Let's Encrypt, WP-CLI, and isolated browser tests.
## Architecture ## Architecture
@@ -12,6 +12,7 @@ browser tests.
- `db`: MariaDB 11.8 LTS, available only on the internal data network. - `db`: MariaDB 11.8 LTS, available only on the internal data network.
- `proxy`: the only published service; routes `azionelab.org` to WordPress and applies - `proxy`: the only published service; routes `azionelab.org` to WordPress and applies
edge security controls. edge security controls.
- `wp-cron`: internal WP-CLI runner for due scheduled WordPress events.
- `certbot`: optional HTTP-01 certificate issue/renewal service. - `certbot`: optional HTTP-01 certificate issue/renewal service.
- `wp-cli`: opt-in bootstrap and maintenance service. - `wp-cli`: opt-in bootstrap and maintenance service.
- `tests/functional`: Playwright tests running only through the public virtual host. - `tests/functional`: Playwright tests running only through the public virtual host.
@@ -61,6 +62,7 @@ Use this checklist for the first run of a new environment.
- set strong `MARIADB_PASSWORD`, `MARIADB_ROOT_PASSWORD`, and `WP_ADMIN_PASSWORD`; - set strong `MARIADB_PASSWORD`, `MARIADB_ROOT_PASSWORD`, and `WP_ADMIN_PASSWORD`;
- set `WP_URL` to the public URL, for example `https://lab.younerd.org`; - set `WP_URL` to the public URL, for example `https://lab.younerd.org`;
- keep `WP_CRON_INTERVAL_SECONDS=300` unless scheduled jobs need a different cadence;
- set `LETSENCRYPT_DOMAIN` to the same hostname when direct Let's Encrypt is used; - set `LETSENCRYPT_DOMAIN` to the same hostname when direct Let's Encrypt is used;
- keep `LETSENCRYPT_STAGING=1` for the first certificate test; - keep `LETSENCRYPT_STAGING=1` for the first certificate test;
- configure `DB_DATA_PATH`, `WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and - configure `DB_DATA_PATH`, `WORDPRESS_DATA_PATH`, `LETSENCRYPT_DATA_PATH`, and
@@ -127,7 +129,7 @@ Use this checklist for the first run of a new environment.
```bash ```bash
docker compose ps docker compose ps
docker compose logs --tail=100 proxy wordpress db certbot docker compose logs --tail=100 proxy wordpress wp-cron db certbot
``` ```
Open the configured `WP_URL` and `/wp-admin/`. Open the configured `WP_URL` and `/wp-admin/`.
@@ -212,6 +214,10 @@ docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
The `docker-compose.test.yml` override intentionally uses disposable test volumes; do The `docker-compose.test.yml` override intentionally uses disposable test volumes; do
not use it for production or staging bootstrap commands. not use it for production or staging bootstrap commands.
`wp-cron` runs internally through WP-CLI every `WP_CRON_INTERVAL_SECONDS` seconds. It
does not publish ports and does not join the public `web` network. Public
`/wp-cron.php` requests remain blocked intentionally.
## Maintenance commands with internet access ## Maintenance commands with internet access
The default `wp-cli` service joins only the internal database network, so it cannot The default `wp-cli` service joins only the internal database network, so it cannot
@@ -232,8 +238,9 @@ Compose file.
## Useful commands ## Useful commands
```bash ```bash
docker compose logs -f proxy wordpress db certbot docker compose logs -f proxy wordpress wp-cron db certbot
docker compose --profile tools run --rm wp-cli -c 'wp plugin list' docker compose --profile tools run --rm wp-cli -c 'wp plugin list'
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
docker compose --profile tools run --rm wp-cli -c 'wp core version' docker compose --profile tools run --rm wp-cli -c 'wp core version'
docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update --version=7.0.2 --force' docker compose -f docker-compose.yml -f docker-compose.maintenance.yml --profile tools run --rm wp-cli -c 'wp core update --version=7.0.2 --force'
+7
View File
@@ -17,6 +17,13 @@ services:
- test_wordpress_data:/var/www/html - test_wordpress_data:/var/www/html
- ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro - ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro
wp-cron:
environment:
WP_CRON_INTERVAL_SECONDS: 300
volumes:
- test_wordpress_data:/var/www/html
- ./wp-cli/cron.sh:/scripts/cron.sh:ro
proxy: proxy:
networks: networks:
web: web:
+41
View File
@@ -81,6 +81,47 @@ services:
- no-new-privileges:true - no-new-privileges:true
pids_limit: 300 pids_limit: 300
wp-cron:
image: wordpress:cli-2.12.0-php8.3
restart: unless-stopped
init: true
user: "33:33"
working_dir: /var/www/html
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-azionelab}
WORDPRESS_DB_USER: ${MARIADB_USER:-azionelab}
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:-replace-with-a-local-password}
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-azl_}
WP_CRON_INTERVAL_SECONDS: ${WP_CRON_INTERVAL_SECONDS:-300}
WP_ENVIRONMENT_TYPE: ${WP_ENVIRONMENT_TYPE:-local}
WP_URL: ${WP_URL:-http://azionelab.org:8080}
HOME: /tmp
HTTP_HOST: ${LETSENCRYPT_DOMAIN:-azionelab.org}
entrypoint: ["/bin/sh"]
command: ["/scripts/cron.sh"]
volumes:
- type: bind
source: ${WORDPRESS_DATA_PATH:-./runtime/wordpress}
target: /var/www/html
bind:
create_host_path: true
- ./wp-cli/cron.sh:/scripts/cron.sh:ro
networks:
- data
depends_on:
db:
condition: service_healthy
wordpress:
condition: service_healthy
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
pids_limit: 100
proxy: proxy:
build: build:
context: ./nginx context: ./nginx
+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 sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
post types so structured content is not lost when changing themes. 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; NGINX is the only public entry point. Public `wp-cron.php` requests stay blocked, while
Certbot provides opt-in direct TLS. Docker networks isolate the database. Runtime state an internal WP-CLI based `wp-cron` service runs due scheduled events over the private
uses host-based bind mounts with a preparation script for ownership/mode, while data network. WP-CLI provides an idempotent opt-in bootstrap; Certbot provides opt-in
functional tests use separate Docker volumes. 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 ## Consequences
@@ -30,6 +32,8 @@ functional tests use separate Docker volumes.
- Core/theme/plugin patches require an image rebuild in production. - Core/theme/plugin patches require an image rebuild in production.
- Fixed participation cards remain code until editorial requirements justify another - Fixed participation cards remain code until editorial requirements justify another
structured content type. structured content type.
- Scheduled WordPress jobs depend on the internal `wp-cron` service instead of visitor
traffic.
## Alternatives considered ## Alternatives considered
@@ -43,8 +47,9 @@ functional tests use separate Docker volumes.
The architecture inherits WordPress's public CMS attack surface. The implementation The architecture inherits WordPress's public CMS attack surface. The implementation
reduces it with network isolation, no direct application port, disabled file editing reduces it with network isolation, no direct application port, disabled file editing
and XML-RPC, production immutability, sanitization/escaping, rate limiting, security and XML-RPC, production immutability, sanitized/escaped rendering, no bundled demo
headers, and fail-closed TLS. Admin MFA and allowlisting remain external controls. plugins, internal-only scheduled jobs, rate limiting, security headers, and fail-closed
TLS. Admin MFA and allowlisting remain external controls.
## Operational impact ## 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 editorial data sources for the homepage, not standalone public routes or REST
collections. Images use WordPress featured images with local SVG fallbacks. collections. Images use WordPress featured images with local SVG fallbacks.
WP-CLI is an opt-in tools-profile service. Its idempotent bootstrap installs WordPress, `wp-cron` is an internal WP-CLI runner on the `data` network. It executes due scheduled
activates the theme, configures the site, and creates realistic demo content. Certbot is events with `wp cron event run --due-now` and keeps public `wp-cron.php` requests
another optional service, enabled only for direct deployments. It shares challenge and blocked. WP-CLI is also available as an opt-in tools-profile service. Its idempotent
certificate volumes with NGINX but has no container-control access. 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, 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 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 NGINX binds to loopback ports 8080/8443. WordPress and MariaDB remain private. The
bootstrap is safe to rerun and does not duplicate demo records. 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 The default persistent paths are host-based bind mounts under `./runtime`. Before the
first start, run: first start, run:
@@ -37,6 +39,9 @@ Required controls:
- either direct Let's Encrypt termination or a trusted external load balancer; - either direct Let's Encrypt termination or a trusted external load balancer;
- off-host database/file backups and monitoring. - 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` 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 is safe only when firewall/network policy makes the load balancer the sole NGINX
caller and it overwrites `X-Forwarded-*` headers. caller and it overwrites `X-Forwarded-*` headers.
+9 -7
View File
@@ -5,9 +5,10 @@
```bash ```bash
docker compose up --build -d docker compose up --build -d
docker compose ps 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 ./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 core version'
docker compose --profile tools run --rm wp-cli -c 'wp cron event list'
docker compose down 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 use the maintenance override for routine bootstrap or inspection commands that
do not need outbound network access. do not need outbound network access.
External `wp-cron.php` requests are blocked to reduce public attack surface. If future External `wp-cron.php` requests are blocked to reduce public attack surface. The
content scheduling, maintenance jobs, or plugin features require WordPress cron, run it default `wp-cron` service runs due scheduled events internally through WP-CLI every
from a controlled host/container cron against the private WordPress service instead of `WP_CRON_INTERVAL_SECONDS` seconds, defaults to 300, and joins only the internal `data`
leaving the public trigger enabled. 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 ## 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 owner or mode; run `./scripts/prepare-host-volumes.sh` after changing paths or image
user IDs. user IDs.
- SMTP is not configured; WordPress password-reset email needs an external mail service. - 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 - WordPress cron is not publicly triggerable; scheduled jobs depend on the internal
cron runner if that feature becomes necessary. `wp-cron` runner being healthy.
- Admin MFA and network allowlisting are deployment concerns and are not bundled. - Admin MFA and network allowlisting are deployment concerns and are not bundled.
- WordPress plugins expand the attack surface; install only reviewed, maintained, - WordPress plugins expand the attack surface; install only reviewed, maintained,
necessary plugins. 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 If DNS still fails with the maintenance override, inspect the host/container DNS
configuration and firewall rules before retrying the update. 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 ## Uploaded image does not appear on the public page
1. Confirm the image was selected in **Appearance > Customize**, not only uploaded in 1. Confirm the image was selected in **Appearance > Customize**, not only uploaded in
+5 -1
View File
@@ -11,6 +11,8 @@
collections. collections.
- File editing is always disabled. Production also disables web-based core, theme, and - File editing is always disabled. Production also disables web-based core, theme, and
plugin changes; patched images are rebuilt and redeployed instead. 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, - XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
direct `wp-config.php` requests, the WordPress readme/license files, direct direct `wp-config.php` requests, the WordPress readme/license files, direct
installation entry points, public mu-plugin directory probing, and external 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, WordPress/Apache retains the capabilities needed by the official image internally,
but no WordPress port is published. A containerized security test fails if the but no WordPress port is published. A containerized security test fails if the
WordPress service is configured with host-published ports. 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 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 outbound internet access, such as WordPress core downloads; do not use it for routine
bootstrap or inspection commands. 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 landmarks, image alternatives, admin routing, security headers, blocked sensitive
routes, blocked uploaded PHP requests, and unknown virtual hosts. Security checks also 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 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 Subjective visual review and a real-device accessibility audit remain manual release
checks. checks.
+74
View File
@@ -27,6 +27,26 @@ grep -q '^FROM wordpress:7\.0\.2-php8\.3-apache$' /workspace/wordpress/Dockerfil
exit 1 exit 1
} }
grep -q 'wp-content/plugins/akismet' /workspace/wordpress/Dockerfile || {
echo "The WordPress image must remove the bundled Akismet plugin." >&2
exit 1
}
grep -q 'wp-content/plugins/hello.php' /workspace/wordpress/Dockerfile || {
echo "The WordPress image must remove the bundled Hello Dolly plugin." >&2
exit 1
}
grep -q 'wp-content/plugins/akismet' /workspace/wordpress/entrypoint-wrapper.sh || {
echo "The WordPress entrypoint must remove Akismet from existing persistent volumes." >&2
exit 1
}
grep -q 'wp-content/plugins/hello.php' /workspace/wordpress/entrypoint-wrapper.sh || {
echo "The WordPress entrypoint must remove Hello Dolly from existing persistent volumes." >&2
exit 1
}
grep -q 'wp-content/(uploads|files)' /workspace/wordpress/apache-hardening.conf || { grep -q 'wp-content/(uploads|files)' /workspace/wordpress/apache-hardening.conf || {
echo "Apache hardening must block PHP execution below uploads/files." >&2 echo "Apache hardening must block PHP execution below uploads/files." >&2
exit 1 exit 1
@@ -76,3 +96,57 @@ grep -q "^[[:space:]]*-[[:space:]]*web$" /workspace/docker-compose.maintenance.y
echo "The maintenance wp-cli service must explicitly opt into the web network." >&2 echo "The maintenance wp-cli service must explicitly opt into the web network." >&2
exit 1 exit 1
} }
grep -Eq "^[[:space:]]{2}wp-cron:" "$compose_file" || {
echo "Compose must define an internal wp-cron service." >&2
exit 1
}
grep -q "wp cron event run --due-now" /workspace/wp-cli/cron.sh || {
echo "The wp-cron runner must execute due WordPress cron events via WP-CLI." >&2
exit 1
}
awk '
/^ wp-cron:/ {
in_wp_cron = 1
has_data_network = 0
has_db_host = 0
next
}
in_wp_cron && /^ [A-Za-z0-9_-]+:/ {
if (!has_data_network) {
print "The wp-cron service must join the internal data network." > "/dev/stderr"
exit 1
}
if (!has_db_host) {
print "The wp-cron service must receive WordPress database environment variables." > "/dev/stderr"
exit 1
}
in_wp_cron = 0
}
in_wp_cron && /^ ports:/ {
print "The wp-cron service must not publish host ports." > "/dev/stderr"
exit 1
}
in_wp_cron && /^[[:space:]]+WORDPRESS_DB_HOST:/ {
has_db_host = 1
}
in_wp_cron && /^ - data$/ {
has_data_network = 1
}
in_wp_cron && /^ - web$/ {
print "The wp-cron service must not join the public web network." > "/dev/stderr"
exit 1
}
END {
if (in_wp_cron && !has_data_network) {
print "The wp-cron service must join the internal data network." > "/dev/stderr"
exit 1
}
if (in_wp_cron && !has_db_host) {
print "The wp-cron service must receive WordPress database environment variables." > "/dev/stderr"
exit 1
}
}
' "$compose_file"
+1
View File
@@ -9,6 +9,7 @@ COPY theme/azionelab /opt/azionelab/theme
COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php
RUN sed -ri 's!^[[:space:]]*CustomLog .*!CustomLog /dev/null combined!' /etc/apache2/sites-available/000-default.conf \ RUN sed -ri 's!^[[:space:]]*CustomLog .*!CustomLog /dev/null combined!' /etc/apache2/sites-available/000-default.conf \
&& rm -rf /usr/src/wordpress/wp-content/plugins/akismet /usr/src/wordpress/wp-content/plugins/hello.php \
&& a2enconf azionelab-apache-hardening \ && a2enconf azionelab-apache-hardening \
&& chmod 755 /usr/local/bin/azionelab-entrypoint \ && chmod 755 /usr/local/bin/azionelab-entrypoint \
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \ && chmod 644 /usr/local/bin/azionelab-healthcheck.php \
+2 -1
View File
@@ -24,7 +24,8 @@ case "$environment" in
esac esac
if [ "${1:-}" = "apache2-foreground" ]; then if [ "${1:-}" = "apache2-foreground" ]; then
mkdir -p /var/www/html/wp-content/themes /var/www/html/wp-content/mu-plugins mkdir -p /var/www/html/wp-content/themes /var/www/html/wp-content/mu-plugins /var/www/html/wp-content/plugins
rm -rf /var/www/html/wp-content/plugins/akismet /var/www/html/wp-content/plugins/hello.php
rm -rf /var/www/html/wp-content/themes/azionelab rm -rf /var/www/html/wp-content/themes/azionelab
cp -a /opt/azionelab/theme /var/www/html/wp-content/themes/azionelab cp -a /opt/azionelab/theme /var/www/html/wp-content/themes/azionelab
cp /opt/azionelab/azionelab-content.php /var/www/html/wp-content/mu-plugins/azionelab-content.php cp /opt/azionelab/azionelab-content.php /var/www/html/wp-content/mu-plugins/azionelab-content.php
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
set -eu
cd /var/www/html
trap 'exit 0' INT TERM
interval="${WP_CRON_INTERVAL_SECONDS:-300}"
case "$interval" in
"" | *[!0-9]*)
echo "WP_CRON_INTERVAL_SECONDS must be a positive integer." >&2
exit 1
;;
esac
if [ "$interval" -lt 60 ]; then
echo "WP_CRON_INTERVAL_SECONDS must be at least 60 seconds." >&2
exit 1
fi
while [ ! -f /var/www/html/wp-load.php ]; do
sleep 10
done
until wp core is-installed >/dev/null 2>&1; do
sleep 10
done
while :; do
if ! wp cron event run --due-now >/dev/null 2>&1; then
echo "Warning: failed to run due WordPress cron events." >&2
fi
sleep "$interval" &
wait "$!"
done