pbs-projects/Tech/Sessions/wp-cron-supercronic-deploy.md

3.7 KiB

project type status path tags created updated
wp-cron-supercronic-deploy session-notes completed Tech/Sessions
pbs
wordpress
traefik
docker
cron
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:

  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

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 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