--- project: wp-cron-supercronic-deploy type: session-notes status: completed path: Tech/Sessions tags: - pbs - wordpress - traefik - docker - cron created: 2026-04-15 updated: 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: 1. **Disable WP-Cron on page load** via `wp-config.php` 2. **Block external access to `wp-cron.php`** at the Traefik middleware layer 3. **Deploy a Supercronic sidecar container** that calls wp-cron on a real schedule via the internal Docker network ## Implementation ### 1. wp-config.php ```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: ```yaml 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: ```bash docker cp wordpress:/var/www/html/wp-config.php /opt/docker/wordpress/wp-config.php ``` ### 2. Traefik middleware — block external `wp-cron.php` ```jinja - 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: ```bash 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`: ```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 `redirectregex` matches path only**, not the full URL — design regex accordingly - **`depends_on` only works within the same Docker Compose project** — for cross-project dependencies, declare the network as `external: true` - **`docker compose up -d --force-recreate `** is the right command to pick up compose file changes (not `restart`, 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