3.7 KiB
| project | type | status | path | tags | created | updated | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| wp-cron-supercronic-deploy | session-notes | completed | Tech/Sessions |
|
2026-04-15 | 2026-04-15 |
WP-Cron → Supercronic Sidecar Deployment
Session notes captured retroactively from March 7 work, before the n8n email pipeline was in place.
Problem
Uptime Kuma status checks were triggering WordPress's built-in WP-Cron on every HTTP request, causing cron jobs to fire unpredictably on page load instead of on a proper schedule. Added unnecessary load and made cron behavior non-deterministic.
Solution
Three-part fix:
- Disable WP-Cron on page load via
wp-config.php - Block external access to
wp-cron.phpat the Traefik middleware layer - Deploy a Supercronic sidecar container that calls wp-cron on a real schedule via the internal Docker network
Implementation
1. wp-config.php
define('ALTERNATE_WP_CRON', false);
define('DISABLE_WP_CRON', true);
wp-config.php is bind-mounted from the host so Ansible can manage it:
volumes:
- wordpress_data:/var/www/html
- ./wp-config.php:/var/www/html/wp-config.php
To pull a fresh copy out of a running container:
docker cp wordpress:/var/www/html/wp-config.php
/opt/docker/wordpress/wp-config.php
2. Traefik middleware — block external wp-cron.php
- traefik.http.routers.wordpress.middlewares=www-redirect,block-wpcron
- traefik.http.middlewares.block-wpcron.redirectregex.regex=^/wp-cron\.php.*
- traefik.http.middlewares.block-wpcron.redirectregex.replacement=/
The router definition must include block-wpcron in the middlewares chain
or Traefik silently does not apply it.
Verify:
curl -Lv https://plantbasedsoutherner.com/wp-cron.php 2>&1 | grep -E
"HTTP|Location"
# Expected: HTTP/2 302 + Location: https://plantbasedsoutherner.com
3. Supercronic sidecar
Project structure:
/opt/docker/wpcron/
Dockerfile
crontab
docker-compose.yml
crontab (set to every minute — Wordfence and Jetpack schedule events at
this frequency):
* * * * * curl -s http://wordpress/wp-cron.php?doing_wp_cron
Dockerfile:
FROM alpine:latest
ARG SUPERCRONIC_VERSION=v0.2.29
ARG SUPERCRONIC_ARCH=amd64
RUN wget -q
https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-${SUPERCRONIC_ARCH}
\
-O /usr/local/bin/supercronic && \
chmod +x /usr/local/bin/supercronic && \
apk add --no-cache curl
COPY crontab /etc/crontab
CMD ["/usr/local/bin/supercronic", "/etc/crontab"]
The sidecar lives in its own compose project. To reach the WordPress
container, the wpcron compose file declares the WordPress network as
external: true.
Bonus Fix — Redis Auth on Production
Discovered during the deploy that the production WordPress container was
missing the Redis password env var. Env vars had been moved to an .env
file on staging but the change never made it back to production. Fixed and
now part of the standard env handoff between environments.
Key Learnings
- Traefik
redirectregexmatches path only, not the full URL — design regex accordingly depends_ononly works within the same Docker Compose project — for cross-project dependencies, declare the network asexternal: truedocker compose up -d --force-recreateis the right command to pick up compose file changes (notrestart, which reuses the old config)- Cron interval matters: Plugins like Wordfence and Jetpack schedule per-minute events, so a 5-minute cron will leave them perpetually behind. Every minute is the right default for this stack.
Status
Deployed to staging then promoted to production. Running cleanly since.
...sent from Jenny & Travis