From d7d3103176d4ad9261370c7560890ec6c131eaf6 Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Sat, 17 Oct 2026 14:07:04 -0400 Subject: [PATCH 1/3] Add Docker local development environment Adds a docker compose setup for working on the plugin locally: WordPress (with Xdebug) with the repo mounted as the plugin, MariaDB, a cron runner so syncs happen outside web requests, a WP-CLI service, optional phpMyAdmin, and a PHPUnit runner that can reproduce any of the CI PHP/WordPress version combinations. A Makefile wraps common tasks and the dev site is pre-configured to sync from rays-house.cablecast.net. Co-Authored-By: Claude Opus 5.5 --- .env.example | 19 ++++++ .gitignore | 6 +- .vscode/launch.json | 14 +++++ Makefile | 51 ++++++++++++++++ README.md | 38 ++++++++++++ bin/dev-setup.sh | 40 ++++++++++++ bin/docker-test.sh | 24 ++++++++ docker-compose.yml | 119 ++++++++++++++++++++++++++++++++++++ docker/db/init.sql | 4 ++ docker/tests/Dockerfile | 11 ++++ docker/wordpress/Dockerfile | 7 +++ docker/wordpress/xdebug.ini | 6 ++ 12 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .vscode/launch.json create mode 100644 Makefile create mode 100755 bin/dev-setup.sh create mode 100755 bin/docker-test.sh create mode 100644 docker-compose.yml create mode 100644 docker/db/init.sql create mode 100644 docker/tests/Dockerfile create mode 100644 docker/wordpress/Dockerfile create mode 100644 docker/wordpress/xdebug.ini diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..20c854d --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Copy to .env to override defaults. All values are optional. + +# Host port for the dev site (http://localhost:8080) +WP_PORT=8080 +# Host port for phpMyAdmin (`make tools`) +PMA_PORT=8081 + +# PHP version for the dev site image (any wordpress:php-apache tag) +WP_PHP_VERSION=8.2 + +# Cablecast server the dev site syncs from +CABLECAST_SERVER=https://rays-house.cablecast.net + +# Xdebug mode for the dev site: off, debug, profile, develop, ... +XDEBUG_MODE=debug + +# Default PHP / WordPress versions for `make test` +PHP_VERSION=8.2 +WP_VERSION=latest diff --git a/.gitignore b/.gitignore index 0181ca1..c7bf86d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,8 @@ composer.lock # IDE .idea/ -.vscode/ +.vscode/* +!.vscode/launch.json *.swp *.swo @@ -21,3 +22,6 @@ Thumbs.db # Logs *.log + +# Docker dev environment +.env diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d82af36 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug (docker)", + "type": "php", + "request": "launch", + "port": 9003, + "pathMappings": { + "/var/www/html/wp-content/plugins/wp-cablecast": "${workspaceFolder}" + } + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3123ee9 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +-include .env + +# Local development helpers. See "Local Development" in README.md. + +PHP ?= $(or $(PHP_VERSION),8.2) +WP ?= $(or $(WP_VERSION),latest) + +COMPOSE = docker compose +WPCLI = $(COMPOSE) run --rm cli + +.PHONY: up down setup wp sync logs debug-log tools test test-matrix test-clean reset + +up: ## Start the dev site and install WordPress on first run + $(COMPOSE) up -d --build + $(WPCLI) sh wp-content/plugins/wp-cablecast/bin/dev-setup.sh + +down: ## Stop containers (keeps data) + $(COMPOSE) --profile tools down + +setup: ## Re-run WordPress install / plugin activation + $(WPCLI) sh wp-content/plugins/wp-cablecast/bin/dev-setup.sh + +wp: ## Run WP-CLI: make wp ARGS="plugin list" + $(WPCLI) wp $(ARGS) + +sync: ## Run the Cablecast sync now instead of waiting for cron + $(WPCLI) wp cron event run cablecast_cron_hook + +logs: ## Tail container logs + $(COMPOSE) logs -f + +debug-log: ## Tail wp-content/debug.log + $(COMPOSE) exec wordpress tail -f wp-content/debug.log + +tools: ## Start phpMyAdmin on http://localhost:8081 + $(COMPOSE) --profile tools up -d phpmyadmin + +test: ## Run PHPUnit: make test [PHP=7.4] [WP=6.2] [ARGS="--filter Foo"] + PHP_VERSION=$(PHP) WP_VERSION=$(WP) $(COMPOSE) run --rm --build tests bash bin/docker-test.sh $(ARGS) + +test-matrix: ## Run the same PHP x WP matrix as CI + @set -e; for php in 7.4 8.0 8.1 8.2; do for wp in 6.2 6.4 latest; do \ + $(MAKE) --no-print-directory test PHP=$$php WP=$$wp; \ + done; done + +test-clean: ## Drop cached WP test suites (e.g. to pick up a new "latest") + $(COMPOSE) down + docker volume rm $$(basename $(CURDIR))_test-cache + +reset: ## Destroy everything, including the database + $(COMPOSE) --profile tools --profile cli --profile tests down -v diff --git a/README.md b/README.md index 95a2a69..22d4d7f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,44 @@ I've set up a simple demo site that I'll be expanding with examples of how to do ## Installation Upload the plugin and activate it. Then in the admin dashboard navigate to `Cablecast Settings`. There is a single input for the `Cablecast Server Address`. This is the url you use to login to Cablecast from the public internet. Example `http://tighty.tv`. +## Local Development +A Docker based dev environment is included. It runs WordPress with this repo mounted as the plugin, a MariaDB database, a container that runs WP-Cron every minute (instead of during page loads), and a PHPUnit runner. + +Requirements: [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or another Docker runtime with `docker compose`) and `make`. PHP and Composer are not needed on your machine. + +```sh +make up # build, start, install WordPress, activate the plugin +``` + +- Site: http://localhost:8080 +- Admin: http://localhost:8080/wp-admin (`admin` / `admin`) +- The plugin is pre-configured to sync from `https://rays-house.cablecast.net`. Copy `.env.example` to `.env` to change the server, ports, or PHP version, then run `make up` again. + +Common commands: + +|Command|Description| +|---|---| +|`make sync`|Run the Cablecast sync now instead of waiting for cron| +|`make wp ARGS="post list --post_type=show"`|Run any WP-CLI command| +|`make debug-log`|Tail `wp-content/debug.log`| +|`make logs`|Tail container logs| +|`make tools`|Start phpMyAdmin on http://localhost:8081| +|`make down`|Stop containers (keeps data)| +|`make reset`|Delete containers and all data| + +### Tests +```sh +make test # PHP 8.2, latest WordPress +make test PHP=7.4 WP=6.2 # any version CI tests against +make test ARGS="--filter SyncLock" # pass args to PHPUnit +make test-matrix # the full CI matrix +``` + +The WordPress test suite for each version is cached in a Docker volume. Run `make test-clean` to re-download it (for example, to pick up a new `latest`). + +### Debugging +Xdebug is installed in the WordPress container and starts when a request carries the `XDEBUG_TRIGGER` cookie (set it with a browser extension such as Xdebug Helper). It connects to port 9003. `.vscode/launch.json` contains the path mappings for VS Code's PHP Debug extension. + ## Important Notes About cron_schedules TL;DR - Follow these instructions for setting up wordpress cron: https://developer.wordpress.org/plugins/cron/hooking-into-the-system-task-scheduler/ diff --git a/bin/dev-setup.sh b/bin/dev-setup.sh new file mode 100755 index 0000000..3ac5344 --- /dev/null +++ b/bin/dev-setup.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +# Installs WordPress and activates the plugin in the docker dev environment. +# Runs inside the `cli` container (`make setup`); safe to re-run. +set -e + +cd /var/www/html + +echo "Waiting for wp-config.php..." +i=0 +until [ -f wp-config.php ]; do + i=$((i + 1)) + if [ $i -gt 60 ]; then + echo "wp-config.php never appeared; is the wordpress container running?" >&2 + exit 1 + fi + sleep 1 +done + +if ! wp core is-installed 2>/dev/null; then + wp core install \ + --url="http://localhost:${WP_PORT:-8080}" \ + --title="Cablecast Dev" \ + --admin_user=admin \ + --admin_password=admin \ + --admin_email=admin@example.com \ + --skip-email +fi + +wp rewrite structure '/%postname%/' --hard +wp plugin activate wp-cablecast + +if [ -n "$CABLECAST_SERVER" ]; then + wp option patch update cablecast_options server "$CABLECAST_SERVER" 2>/dev/null \ + || wp option patch insert cablecast_options server "$CABLECAST_SERVER" +fi + +echo +echo "Site: http://localhost:${WP_PORT:-8080}" +echo "Admin: http://localhost:${WP_PORT:-8080}/wp-admin (admin / admin)" +echo "Cablecast server: $(wp option pluck cablecast_options server)" diff --git a/bin/docker-test.sh b/bin/docker-test.sh new file mode 100755 index 0000000..2c49cec --- /dev/null +++ b/bin/docker-test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Runs the PHPUnit suite inside the `tests` container (`make test`). +# WordPress core + test library are cached per WP version in the test-cache volume. +set -e + +WP_VERSION=${WP_VERSION:-latest} +PHP_VERSION=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') + +export WP_CORE_DIR=/cache/wp/$WP_VERSION/core +export WP_TESTS_DIR=/cache/wp/$WP_VERSION/tests-lib + +echo "PHP $PHP_VERSION / WordPress $WP_VERSION" + +# composer.lock isn't committed, so re-resolve when the PHP version changes. +if [ "$(cat vendor/.php-version 2>/dev/null)" != "$PHP_VERSION" ]; then + composer update --prefer-dist --no-progress --no-interaction + echo "$PHP_VERSION" > vendor/.php-version +fi + +if [ ! -f "$WP_TESTS_DIR/wp-tests-config.php" ]; then + bash bin/install-wp-tests.sh wordpress_test root root db "$WP_VERSION" true +fi + +composer test -- "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..57c14b4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +# Local development environment. See "Local Development" in README.md. + +x-wp-env: &wp-env + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: wordpress + WORDPRESS_DB_NAME: wordpress + WORDPRESS_DEBUG: 1 + WORDPRESS_CONFIG_EXTRA: | + define('WP_ENVIRONMENT_TYPE', 'local'); + define('WP_DEBUG_LOG', true); + define('WP_DEBUG_DISPLAY', false); + define('DISABLE_WP_CRON', true); + +x-wp-volumes: &wp-volumes + - wordpress:/var/www/html + - .:/var/www/html/wp-content/plugins/wp-cablecast + +x-wp-cli: &wp-cli + image: wordpress:cli + # Match the apache image's www-data uid so files in the shared volume stay writable. + user: "33:33" + environment: + <<: *wp-env + WP_CLI_CACHE_DIR: /tmp/wp-cli-cache + WP_PORT: ${WP_PORT:-8080} + CABLECAST_SERVER: ${CABLECAST_SERVER:-https://rays-house.cablecast.net} + volumes: *wp-volumes + depends_on: + db: + condition: service_healthy + wordpress: + condition: service_started + +services: + db: + image: mariadb:10.11 + environment: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: wordpress + MARIADB_USER: wordpress + MARIADB_PASSWORD: wordpress + volumes: + - db:/var/lib/mysql + - ./docker/db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 5s + retries: 20 + + wordpress: + build: + context: docker/wordpress + args: + PHP_VERSION: ${WP_PHP_VERSION:-8.2} + ports: + - "${WP_PORT:-8080}:80" + environment: + <<: *wp-env + XDEBUG_MODE: ${XDEBUG_MODE:-debug} + volumes: *wp-volumes + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + db: + condition: service_healthy + + # One-off WP-CLI commands: `make wp ARGS="plugin list"` + cli: + <<: *wp-cli + profiles: ["cli"] + + # Runs WP-Cron from outside web requests, as the README recommends for production. + cron: + <<: *wp-cli + command: > + sh -c 'while true; do + wp core is-installed 2>/dev/null && wp cron event run --due-now --quiet; + sleep 60; + done' + restart: unless-stopped + + # PHPUnit runner: `make test` / `make test PHP=7.4 WP=6.2` + tests: + build: + context: docker/tests + args: + PHP_VERSION: ${PHP_VERSION:-8.2} + image: wp-cablecast-tests:php${PHP_VERSION:-8.2} + profiles: ["tests"] + environment: + WP_VERSION: ${WP_VERSION:-latest} + COMPOSER_CACHE_DIR: /cache/composer + volumes: + - .:/app + - test-cache:/cache + command: bash bin/docker-test.sh + depends_on: + db: + condition: service_healthy + + phpmyadmin: + image: phpmyadmin:5 + profiles: ["tools"] + ports: + - "${PMA_PORT:-8081}:80" + environment: + PMA_HOST: db + PMA_USER: root + PMA_PASSWORD: root + depends_on: + db: + condition: service_healthy + +volumes: + db: + wordpress: + test-cache: diff --git a/docker/db/init.sql b/docker/db/init.sql new file mode 100644 index 0000000..a6eb662 --- /dev/null +++ b/docker/db/init.sql @@ -0,0 +1,4 @@ +-- The dev site database (`wordpress`) is created by the image; this adds the +-- database used by the PHPUnit suite. +CREATE DATABASE IF NOT EXISTS wordpress_test; +GRANT ALL PRIVILEGES ON wordpress_test.* TO 'wordpress'@'%'; diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile new file mode 100644 index 0000000..67897ad --- /dev/null +++ b/docker/tests/Dockerfile @@ -0,0 +1,11 @@ +ARG PHP_VERSION=8.2 +FROM php:${PHP_VERSION}-cli + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git unzip subversion \ + && rm -rf /var/lib/apt/lists/* \ + && docker-php-ext-install mysqli + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +WORKDIR /app diff --git a/docker/wordpress/Dockerfile b/docker/wordpress/Dockerfile new file mode 100644 index 0000000..33f9146 --- /dev/null +++ b/docker/wordpress/Dockerfile @@ -0,0 +1,7 @@ +ARG PHP_VERSION=8.2 +FROM wordpress:php${PHP_VERSION}-apache + +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug + +COPY xdebug.ini /usr/local/etc/php/conf.d/zz-xdebug.ini diff --git a/docker/wordpress/xdebug.ini b/docker/wordpress/xdebug.ini new file mode 100644 index 0000000..e123759 --- /dev/null +++ b/docker/wordpress/xdebug.ini @@ -0,0 +1,6 @@ +; Mode comes from the XDEBUG_MODE env var (see docker-compose.yml). +; Start a session with the browser extension / XDEBUG_TRIGGER cookie. +xdebug.start_with_request=trigger +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.log_level=0 From 22a67b0a9accd33717e21b7f91bc8a444ae8fe96 Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Sat, 17 Oct 2026 16:18:08 -0400 Subject: [PATCH 2/3] Fix issues found running the Docker dev environment - Use MariaDB 11.8: WP-CLI's MariaDB client requires TLS, which 11.4+ servers enable automatically (wp db commands failed on 10.11). - Point apt at archive.debian.org for bullseye-based PHP 7.4/8.0 test images, since bullseye is EOL. - Drop --hard from the permalink setup; the wordpress image already ships a rewrite-ready .htaccess and WP-CLI warned it couldn't write one. Co-Authored-By: Claude Opus 5.5 --- bin/dev-setup.sh | 3 ++- docker-compose.yml | 3 ++- docker/tests/Dockerfile | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bin/dev-setup.sh b/bin/dev-setup.sh index 3ac5344..ce57de5 100755 --- a/bin/dev-setup.sh +++ b/bin/dev-setup.sh @@ -26,7 +26,8 @@ if ! wp core is-installed 2>/dev/null; then --skip-email fi -wp rewrite structure '/%postname%/' --hard +# The wordpress image ships a rewrite-ready .htaccess, so no --hard needed. +wp rewrite structure '/%postname%/' wp plugin activate wp-cablecast if [ -n "$CABLECAST_SERVER" ]; then diff --git a/docker-compose.yml b/docker-compose.yml index 57c14b4..aa3674b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,8 @@ x-wp-cli: &wp-cli services: db: - image: mariadb:10.11 + # 11.4+ enables TLS out of the box, which WP-CLI's MariaDB client requires. + image: mariadb:11.8 environment: MARIADB_ROOT_PASSWORD: root MARIADB_DATABASE: wordpress diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile index 67897ad..895b0f9 100644 --- a/docker/tests/Dockerfile +++ b/docker/tests/Dockerfile @@ -1,6 +1,12 @@ ARG PHP_VERSION=8.2 FROM php:${PHP_VERSION}-cli +# Older PHP images (7.4, 8.0) are built on Debian bullseye, which is EOL and +# only available from the archive. +RUN if grep -q bullseye /etc/os-release; then \ + sed -i -e 's|deb.debian.org|archive.debian.org|g' -e '/bullseye-updates/d' /etc/apt/sources.list; \ + fi + RUN apt-get update \ && apt-get install -y --no-install-recommends git unzip subversion \ && rm -rf /var/lib/apt/lists/* \ From 26fab5a948a1b78040339cdc9aa81aa77c780002 Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Sat, 17 Oct 2026 16:30:00 -0400 Subject: [PATCH 3/3] Support reaching the dev site through Tailscale Serve and other proxies WordPress now derives its URL from the request host (honoring X-Forwarded-Proto), so it works at localhost and behind a proxy without redirecting. Adds make tailscale / tailscale-off to share the site on the tailnet over HTTPS. Co-Authored-By: Claude Opus 5.5 --- Makefile | 10 +++++++++- README.md | 3 +++ docker-compose.yml | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3123ee9..717bf75 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,13 @@ PHP ?= $(or $(PHP_VERSION),8.2) WP ?= $(or $(WP_VERSION),latest) +WP_PORT ?= 8080 +TS_PORT ?= 8443 COMPOSE = docker compose WPCLI = $(COMPOSE) run --rm cli -.PHONY: up down setup wp sync logs debug-log tools test test-matrix test-clean reset +.PHONY: up down setup wp sync logs debug-log tools tailscale tailscale-off test test-matrix test-clean reset up: ## Start the dev site and install WordPress on first run $(COMPOSE) up -d --build @@ -35,6 +37,12 @@ debug-log: ## Tail wp-content/debug.log tools: ## Start phpMyAdmin on http://localhost:8081 $(COMPOSE) --profile tools up -d phpmyadmin +tailscale: ## Share the dev site on your tailnet over HTTPS (port TS_PORT, default 8443) + tailscale serve --bg --https=$(TS_PORT) http://localhost:$(WP_PORT) + +tailscale-off: ## Stop sharing the dev site on your tailnet + tailscale serve --https=$(TS_PORT) off + test: ## Run PHPUnit: make test [PHP=7.4] [WP=6.2] [ARGS="--filter Foo"] PHP_VERSION=$(PHP) WP_VERSION=$(WP) $(COMPOSE) run --rm --build tests bash bin/docker-test.sh $(ARGS) diff --git a/README.md b/README.md index 22d4d7f..df9ec21 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,12 @@ Common commands: |`make debug-log`|Tail `wp-content/debug.log`| |`make logs`|Tail container logs| |`make tools`|Start phpMyAdmin on http://localhost:8081| +|`make tailscale`|Share the site on your tailnet at `https://..ts.net:8443` (`make tailscale-off` to stop)| |`make down`|Stop containers (keeps data)| |`make reset`|Delete containers and all data| +WordPress uses whatever host it's reached by, so the site also works behind a proxy such as Tailscale Serve or ngrok without changing its URL. `make tailscale` uses [Tailscale Serve](https://tailscale.com/kb/1312/serve), which also gets around the macOS firewall blocking direct tailnet connections to Docker ports. + ### Tests ```sh make test # PHP 8.2, latest WordPress diff --git a/docker-compose.yml b/docker-compose.yml index aa3674b..5f33636 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,14 @@ x-wp-env: &wp-env define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); define('DISABLE_WP_CRON', true); + // Use whatever host the site is reached by (localhost, a Tailscale/ngrok proxy, ...). + if (!empty($$_SERVER['HTTP_HOST'])) { + $$https = (!empty($$_SERVER['HTTPS']) && $$_SERVER['HTTPS'] !== 'off') + || ($$_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; + if ($$https) { $$_SERVER['HTTPS'] = 'on'; } + define('WP_HOME', ($$https ? 'https' : 'http') . '://' . $$_SERVER['HTTP_HOST']); + define('WP_SITEURL', WP_HOME); + } x-wp-volumes: &wp-volumes - wordpress:/var/www/html