generated from bisco/codex-bootstrap
feat: add internal WordPress cron runner
This commit is contained in:
@@ -11,6 +11,7 @@ WP_TITLE=Azione!Lab
|
||||
WP_ADMIN_USER=azionelab-admin
|
||||
WP_ADMIN_PASSWORD=replace-with-a-local-admin-password
|
||||
WP_ADMIN_EMAIL=admin@example.org
|
||||
WP_CRON_INTERVAL_SECONDS=300
|
||||
NGINX_BIND_ADDRESS=127.0.0.1
|
||||
NGINX_HTTP_PORT=8080
|
||||
NGINX_HTTPS_PORT=8443
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
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
|
||||
Docker Compose provides MariaDB, NGINX, optional Let's Encrypt, WP-CLI, and isolated
|
||||
browser tests.
|
||||
Docker Compose provides MariaDB, NGINX, an internal WordPress cron runner, optional
|
||||
Let's Encrypt, WP-CLI, and isolated browser tests.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -12,6 +12,7 @@ browser tests.
|
||||
- `db`: MariaDB 11.8 LTS, available only on the internal data network.
|
||||
- `proxy`: the only published service; routes `azionelab.org` to WordPress and applies
|
||||
edge security controls.
|
||||
- `wp-cron`: internal WP-CLI runner for due scheduled WordPress events.
|
||||
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
||||
- `wp-cli`: opt-in bootstrap and maintenance service.
|
||||
- `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 `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;
|
||||
- keep `LETSENCRYPT_STAGING=1` for the first certificate test;
|
||||
- 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
|
||||
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/`.
|
||||
@@ -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
|
||||
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
|
||||
|
||||
The default `wp-cli` service joins only the internal database network, so it cannot
|
||||
@@ -232,8 +238,9 @@ Compose file.
|
||||
## Useful commands
|
||||
|
||||
```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 cron event list'
|
||||
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'
|
||||
|
||||
|
||||
@@ -17,6 +17,13 @@ services:
|
||||
- test_wordpress_data:/var/www/html
|
||||
- ./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:
|
||||
networks:
|
||||
web:
|
||||
|
||||
@@ -81,6 +81,47 @@ services:
|
||||
- no-new-privileges:true
|
||||
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:
|
||||
build:
|
||||
context: ./nginx
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -27,6 +27,26 @@ grep -q '^FROM wordpress:7\.0\.2-php8\.3-apache$' /workspace/wordpress/Dockerfil
|
||||
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 || {
|
||||
echo "Apache hardening must block PHP execution below uploads/files." >&2
|
||||
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
|
||||
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"
|
||||
|
||||
@@ -9,6 +9,7 @@ COPY theme/azionelab /opt/azionelab/theme
|
||||
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 \
|
||||
&& rm -rf /usr/src/wordpress/wp-content/plugins/akismet /usr/src/wordpress/wp-content/plugins/hello.php \
|
||||
&& a2enconf azionelab-apache-hardening \
|
||||
&& chmod 755 /usr/local/bin/azionelab-entrypoint \
|
||||
&& chmod 644 /usr/local/bin/azionelab-healthcheck.php \
|
||||
|
||||
@@ -24,7 +24,8 @@ case "$environment" in
|
||||
esac
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user