# 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). ## Database — MySQL or SQLite The code runs against either MySQL 8.0+ or SQLite 3.24+. The `config.php.example` defaults to **MySQL** because the `php-sqlite3` extension is unmaintained on some older distributions (Ubuntu 20.04 + Ondrej Sury's PHP 8.3 build is the case that bit us — the SQLite extension is no longer packaged for that combination, and you can't upgrade the OS without disturbing other live services). ### MySQL setup As root on the DB server (root via socket auth works on a stock Ubuntu MySQL): ```sql CREATE DATABASE att_hc DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'att_hc'@'localhost' IDENTIFIED BY 'STRONG_RANDOM_PASSWORD'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, REFERENCES ON att_hc.* TO 'att_hc'@'localhost'; FLUSH PRIVILEGES; ``` The grant is scoped to `att_hc.*` only — the user can't see or touch other databases on a shared box. Generate the password with: ```sh php -r 'echo bin2hex(random_bytes(18)), PHP_EOL;' ``` Then in `config.php`: ```php 'db_dsn' => 'mysql:host=localhost;dbname=att_hc;charset=utf8mb4', 'db_user' => 'att_hc', 'db_pass' => 'STRONG_RANDOM_PASSWORD', ``` The schema is created on the first authenticated request (the migration runner runs CREATE TABLE IF NOT EXISTS for everything). You can pre-trigger it by hitting any authenticated endpoint, e.g.: ```sh curl -s -H "Authorization: Bearer " \ https://healthcheck.example.com/sites?limit=1 ``` After which `SHOW TABLES IN att_hc;` should list `healthchecks`, `step_updates`, `migrations`. ### SQLite setup (if `pdo_sqlite` is available) Set in `config.php`: ```php 'db_dsn' => 'sqlite:' . __DIR__ . '/data/att_hc.sqlite', 'db_user' => null, 'db_pass' => null, ``` The DB file is created automatically inside `data/` on first request, so that directory needs to be writable by the web user. See the install snippet below for the `mkdir + chmod 770` step. ## First-time install ```sh # 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 + DB creds # Only needed for the SQLite backend — MySQL skips this: mkdir -p data sudo chown -R www-data:www-data data/ sudo chmod 770 data/ sudo chown www-data:www-data config.php sudo chmod 640 config.php ``` ## Apache vhost (example) ```apache ServerName healthcheck-history.example.com DocumentRoot /home/www/healthcheck/public AllowOverride All Require all granted # 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 ``` ## nginx (example) ```nginx 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: ```sh 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: ```php define('ATT_HC_API_URL', 'https://healthcheck-history.example.com'); define('ATT_HC_API_KEY', ''); ``` ## Backups The SQLite DB is a single file. Snapshot it on a cron: ```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. ```sh rsync -avz --delete --exclude='data/' --exclude='config.php' \ ~/dev/att-site-healthcheck/server/ \ user@vps:/home/www/healthcheck/ ``` ## Quick health check ```sh curl https://healthcheck-history.example.com/ # → {"ok":true,"service":"att-site-healthcheck-server","version":"..."} ```