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>
170 lines
5.5 KiB
Markdown
170 lines
5.5 KiB
Markdown
# 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
|
|
|
|
```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
|
|
|
|
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)
|
|
|
|
```apache
|
|
<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)
|
|
|
|
```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', '<the long random string from config.php>');
|
|
```
|
|
|
|
## 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":"..."}
|
|
```
|