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>
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/neverreturn 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 atry_filesfallback. -
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 to750or700, which blocks the web user from traversing into the app.sudo chmod 755 /home/wwwonce is usually enough./var/wwwdoesn'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'tcp healthcheck.conf healthcheck.conf.bakinside that directory — the backup becomes a second vhost claiming the sameserver_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 outsidesites-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":"..."}