Files
wp-healthcheck/server/DEPLOY.md
Steve Hanlon f6d2573a35 DEPLOY.md: version-agnostic FPM socket + PHP 8.1+ requirement louder + sites-enabled backup gotcha
Two issues hit during a real deploy:

1. The nginx example pinned fastcgi_pass to /run/php/php8.2-fpm.sock.
   When the target box runs a different PHP-FPM (8.3 here), the socket
   doesn't exist and nginx 502s. Switch to /run/php/php-fpm.sock (the
   alternatives symlink installed by Ondrej Sury's packaging) so the
   default config works against whatever current PHP is installed,
   with a comment explaining the fallback for boxes without the symlink.

2. The PHP version requirement was buried in a one-liner. Promote it to
   a multi-line note that names the specific 8.x features used, says
   loudly that 7.x will not parse the code, and points at Sury's repo
   for boxes stuck on an old default.

Also added a callout warning against cp foo.conf foo.conf.bak inside
sites-enabled/, since nginx loads every file in that directory and the
backup becomes a second vhost claiming the same server_name (silently
shadowed via the "conflicting server name … ignored" warning).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-29 13:41:54 +01:00

5.5 KiB

Deploying the ATT Healthcheck Server

Single-tenant PHP/SQLite app. Tiny — runs comfortably on the smallest VPS tier.

Requirements

  • PHP 8.1+ is required. The code uses typed properties, str_starts_with, mixed/never return types, named arguments, and constructor property promotion. PHP 7.x will not even parse the source files. PHP 7.2 has also been EOL since November 2020 — don't run a public service on it.

    Confirm with php -v. If the box is on an older default, install a current PHP alongside it (Ondrej Sury's repo is the standard on Debian/Ubuntu) and point this vhost at the new FPM socket.

  • PHP extensions: pdo, pdo_sqlite, json, mbstring (all standard).

  • Apache + mod_rewrite, or nginx with a try_files fallback.

  • HTTPS terminating in front of PHP. Plain HTTP is technically accepted, but the plugin client will refuse to talk to non-https:// endpoints.

Layout on the server

Anywhere works — every path inside the app is resolved relative to its own files. Example using /home/www/healthcheck:

/home/www/healthcheck/
├── public/              ← docroot
│   ├── .htaccess
│   └── index.php
├── src/
├── migrations/
├── data/                ← SQLite file lives here (writable by web user)
├── config.php           ← deployment-specific, NOT in git
└── config.php.example

Perm gotcha for /home/... locations: home directories often default to 750 or 700, which blocks the web user from traversing into the app. sudo chmod 755 /home/www once is usually enough. /var/www doesn't have this problem (always world-readable by default).

First-time install

# As you, on the server:
sudo mkdir -p /home/www/healthcheck
sudo chown $USER:www-data /home/www/healthcheck
sudo chmod 755 /home/www                           # see perm gotcha above

# From your dev box:
rsync -avz --exclude='data/' --exclude='config.php' \
    ~/dev/att-site-healthcheck/server/ \
    user@vps:/home/www/healthcheck/

# Back on the server:
cd /home/www/healthcheck
cp config.php.example config.php
php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;'   # generate api_key
$EDITOR config.php                                   # paste it in

mkdir -p data
sudo chown -R www-data:www-data data/ config.php
sudo chmod 640 config.php
sudo chmod 770 data/

Apache vhost (example)

<VirtualHost *:443>
    ServerName healthcheck-history.example.com
    DocumentRoot /home/www/healthcheck/public

    <Directory /home/www/healthcheck/public>
        AllowOverride All
        Require all granted
    </Directory>

    # Pass Authorization header through to PHP — Apache strips it by default with FPM.
    SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

    SSLEngine on
    SSLCertificateFile      /etc/letsencrypt/live/healthcheck-history.example.com/fullchain.pem
    SSLCertificateKeyFile   /etc/letsencrypt/live/healthcheck-history.example.com/privkey.pem
</VirtualHost>

nginx (example)

server {
    listen 443 ssl http2;
    server_name healthcheck-history.example.com;
    root /home/www/healthcheck/public;
    index index.php;

    location / {
        try_files $uri /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        # Version-agnostic — Ondrej Sury's packaging installs this symlink
        # pointing at whichever php-fpm is the current "alternative". If the
        # symlink doesn't exist on your box, hardcode e.g. php8.3-fpm.sock.
        # Check with: ls -la /run/php/
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTP_AUTHORIZATION $http_authorization;
    }

    ssl_certificate     /etc/letsencrypt/live/healthcheck-history.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/healthcheck-history.example.com/privkey.pem;
}

Gotcha: nginx loads every file in sites-enabled/ regardless of extension. Don't cp healthcheck.conf healthcheck.conf.bak inside that directory — the backup becomes a second vhost claiming the same server_name, and nginx silently ignores one of them ("conflicting server name … ignored" in the error log) which manifests as the new config not taking effect. Back up outside sites-enabled/.

TLS

Use Let's Encrypt + certbot. Example:

sudo certbot --apache -d healthcheck-history.example.com
# or for nginx:
sudo certbot --nginx -d healthcheck-history.example.com

Configure the plugin to talk to it

In wp-config.php on each WordPress install:

define('ATT_HC_API_URL', 'https://healthcheck-history.example.com');
define('ATT_HC_API_KEY', '<the long random string from config.php>');

Backups

The SQLite DB is a single file. Snapshot it on a cron:

# Daily at 03:15, rotate 14 days. SQLite-safe online backup.
15 3 * * * /usr/bin/sqlite3 /home/www/healthcheck/data/att_hc.sqlite ".backup '/var/backups/att-hc/att_hc-$(date +\%F).sqlite'" && find /var/backups/att-hc -name 'att_hc-*.sqlite' -mtime +14 -delete

Pull those backups offsite with rsync / rclone to whatever you already use.

Updating

Just rsync the source again — migrations run on first request after deploy.

rsync -avz --delete --exclude='data/' --exclude='config.php' \
    ~/dev/att-site-healthcheck/server/ \
    user@vps:/home/www/healthcheck/

Quick health check

curl https://healthcheck-history.example.com/
# → {"ok":true,"service":"att-site-healthcheck-server","version":"..."}