feat: add Sibiu Independent stack migration plan (Debian/AApanel → FreeBSD 14.x)
This commit is contained in:
725
README.md
Normal file
725
README.md
Normal file
@@ -0,0 +1,725 @@
|
||||
# Sibiu Independent — Stack Migration
|
||||
## May 2026
|
||||
|
||||
Migration of `sibiuindependent.ro` from Debian 12 + AApanel to FreeBSD 14.x minimal stack.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture
|
||||
|
||||
```
|
||||
[internet] → [Nginx Proxy Manager] → [dracula, Tailscale] → [transilvan, Tailscale]
|
||||
```
|
||||
|
||||
| Node | Role | OS | Specs |
|
||||
|---|---|---|---|
|
||||
| dracula | Web | Debian 12 | 16 vCPU, 32GB RAM, 245GB disk |
|
||||
| transilvan | Database | Debian 12 | 4 vCPU, 32GB RAM, 245GB disk |
|
||||
|
||||
Both nodes are VMs running on separate bare-metal hosts within the same datacenter (~1ms RTT). All inter-service traffic runs over Tailscale (WireGuard). The NPM node handles TLS termination and public exposure. The web and DB nodes have no public-facing ports other than what Tailscale exposes.
|
||||
|
||||
---
|
||||
|
||||
## Current Stack Audit
|
||||
|
||||
### dracula (web)
|
||||
|
||||
| Component | Version / Detail |
|
||||
|---|---|
|
||||
| OS | Debian GNU/Linux 12 (bookworm) |
|
||||
| Web server | nginx 1.30.1 |
|
||||
| PHP | 8.5.2 (AApanel custom build) + FPM |
|
||||
| CMS | WordPress, single site: `sibiuindependent.ro` |
|
||||
| Theme | Newspaper / TagDiv (td-cloud-library, td-composer, td-standard-pack) |
|
||||
| Caching | W3 Total Cache + Memcached (page + object cache) |
|
||||
| Sessions | PHP file sessions |
|
||||
| Media | wp-content/uploads 6.5GB on disk, S3-compatible offload active |
|
||||
| Cache on disk | 4.8GB (W3TC page cache — throwaway) |
|
||||
| WP code | ~700MB (core + plugins + themes) |
|
||||
| DB connection | `DB_HOST=transilvan` resolved via /etc/hosts → Tailscale IP |
|
||||
| Monitoring | Telegraf, Beszel agent, Wazuh agent, Five Nines agent |
|
||||
| Security | Fail2ban, SSHGuard, sshguard |
|
||||
| Mail | Postfix + MTA-STS (SMTP plugin used, local MTA not needed) |
|
||||
| Panel | AApanel (btpanel) |
|
||||
| Tailscale IP | 100.99.157.56 |
|
||||
|
||||
**Active PHP extensions:** bcmath, curl, exif, fileinfo, gd, imagick, intl, json, mbstring, memcached, mysqli, mysqlnd, opcache, openssl, pcntl, pdo, pdo_mysql, pdo_sqlite, posix, soap, sockets, sodium, xml, xmlreader, xmlwriter, xsl, zip
|
||||
|
||||
### transilvan (db)
|
||||
|
||||
| Component | Version / Detail |
|
||||
|---|---|
|
||||
| OS | Debian GNU/Linux 12 (bookworm) |
|
||||
| Database | MariaDB 10.11.14 LTS |
|
||||
| Database name | `sql_sibiuindepen` |
|
||||
| DB user | `sql_sibiuindepen` |
|
||||
| Data directory | /www/server/data (~9.6GB raw) |
|
||||
| Config | /etc/mysql/mariadb.cnf + conf.d/ |
|
||||
| Monitoring | Telegraf, Beszel agent, Wazuh agent, Five Nines agent |
|
||||
| Panel | AApanel (btpanel) |
|
||||
| Binlog | Active (mysql-bin.*, high sequence numbers) |
|
||||
|
||||
---
|
||||
|
||||
## OS Evaluation
|
||||
|
||||
### Candidates assessed: OpenBSD, FreeBSD, Gentoo, Alpine Linux
|
||||
|
||||
### OpenBSD
|
||||
- **Security:** Best in class — pledge(2), unveil(2), W^X enforced, secure malloc, pf
|
||||
- **PHP 8.5:** Not in packages (ports have 8.4). Source build possible but complex dependency tree
|
||||
- **php-memcached:** Problematic (libmemcached issues on OpenBSD)
|
||||
- **MySQL 8.x:** Not in packages (MariaDB only)
|
||||
- **PHP JIT:** Conflicts with W^X; must disable (negligible for WordPress)
|
||||
- **Monitoring agents:** Wazuh: no official support. Telegraf: community build
|
||||
- **Verdict:** Excellent security philosophy, but too much friction for this specific stack (PHP 8.5, MySQL 8.x, imagick). Viable only if stripping monitoring stack and accepting PHP 8.4 + Redis instead of memcached
|
||||
|
||||
### FreeBSD
|
||||
- **Security:** pf, Capsicum, jails, strong defaults
|
||||
- **PHP 8.5:** Available in ports (lang/php85). All extensions in ports
|
||||
- **MySQL 8.4 LTS:** Available as databases/mysql84-server
|
||||
- **imagick, intl, soap, redis:** All in ports
|
||||
- **ZFS:** Native. Game-changer for database node (atomic snapshots, checksumming, lz4 compression)
|
||||
- **Monitoring:** Telegraf official package. Tailscale official package. Beszel: Go binary works
|
||||
- **Wazuh:** Community source build only — dropped per requirements
|
||||
- **Verdict:** Strongest all-round candidate. PHP 8.5 + all extensions + MySQL 8.4 + ZFS + pf + official Tailscale
|
||||
|
||||
### Gentoo
|
||||
- **Minimalism:** Genuine — USE flags compile exactly what is needed, OpenRC available
|
||||
- **PHP 8.5:** In portage (rolling release guarantees latest)
|
||||
- **MySQL 8.4:** In portage
|
||||
- **All agents:** Still Linux, everything works natively
|
||||
- **Maintenance:** High ongoing cost — every update is a compile run. Production liability
|
||||
- **Verdict:** Philosophically compelling, operationally expensive for a production news site
|
||||
|
||||
### Alpine Linux
|
||||
- **Minimalism:** musl libc + busybox, ~130MB base
|
||||
- **PHP 8.5:** Community repos, most extensions packaged
|
||||
- **Init:** OpenRC, no systemd
|
||||
- **Migration effort:** Lowest — still Linux, apk replaces apt, paths similar
|
||||
- **Verdict:** Best minimal Linux option; easier than FreeBSD but fewer operational benefits (no ZFS, no jails)
|
||||
|
||||
### Decision: **FreeBSD 14.x on both nodes**
|
||||
|
||||
Tiebreaker: ZFS native on the DB node. `zfs snapshot` before any migration step, before any MySQL upgrade, before any schema change — this alone justifies the choice. PHP 8.5 builds from ports cleanly. MySQL 8.4 LTS is in pkg. All required PHP extensions are in ports. pf is simpler and more powerful than nftables for these firewall rules. Tailscale has official FreeBSD support.
|
||||
|
||||
---
|
||||
|
||||
## Target Architecture
|
||||
|
||||
```
|
||||
[internet] → [NPM node, Tailscale] → [dracula-new FreeBSD, Tailscale] → [transilvan-new FreeBSD, Tailscale]
|
||||
|
||||
dracula-new (FreeBSD 14.x) transilvan-new (FreeBSD 14.x)
|
||||
────────────────────────── ─────────────────────────────
|
||||
nginx (pkg) ZFS pool → /var/db/mysql
|
||||
PHP 8.5 (ports build) recordsize=16K (InnoDB optimal)
|
||||
gd, imagick, intl, mbstring, compression=lz4
|
||||
opcache, pdo_mysql, redis, primarycache=metadata
|
||||
soap, sockets, sodium, MySQL 8.4 LTS (ports)
|
||||
xml, zip, exif, fileinfo pf: 3306 from dracula Tailscale IP only
|
||||
nginx fastcgi_cache (filesystem) Tailscale
|
||||
Redis (pkg, unix socket) SSH on Tailscale only
|
||||
pf: 80 from NPM Tailscale IP only Telegraf, Beszel
|
||||
Tailscale
|
||||
SSH on Tailscale only
|
||||
Telegraf, Beszel
|
||||
```
|
||||
|
||||
### What is dropped
|
||||
| Component | Reason |
|
||||
|---|---|
|
||||
| AApanel / btpanel | Replaced by direct config management |
|
||||
| W3 Total Cache | Replaced by nginx fastcgi_cache + Redis Object Cache plugin |
|
||||
| Memcached | Replaced by Redis (unix socket, faster, simpler) |
|
||||
| Fail2ban / SSHGuard | No public SSH port, Tailscale-only |
|
||||
| Postfix / sendmail | SMTP plugin in use, no local MTA needed |
|
||||
| Wazuh agent | Not mandatory per requirements |
|
||||
| Five Nines agent | Not mandatory per requirements |
|
||||
| memory-cleanup.sh | Linux /proc hack, was on wrong server anyway |
|
||||
| ngxblocker | Not applicable to new stack |
|
||||
| AApanel cron scripts | acme renewal, site_total, backup.py — all panel-specific |
|
||||
|
||||
### Caching architecture change
|
||||
| Layer | Old | New |
|
||||
|---|---|---|
|
||||
| Page cache | W3TC on-disk (4.8GB, PHP-generated) | nginx fastcgi_cache (filesystem, PHP bypassed entirely on HIT) |
|
||||
| Object cache | W3TC + Memcached (TCP) | Redis Object Cache plugin + Redis (unix socket) |
|
||||
| Bytecode cache | OPcache | OPcache (unchanged, JIT disabled — no benefit for WordPress) |
|
||||
| Sessions | PHP file sessions | Redis via session.save_path unix socket |
|
||||
|
||||
---
|
||||
|
||||
## Migration Data Inventory
|
||||
|
||||
| Component | Size | Action |
|
||||
|---|---|---|
|
||||
| WordPress core + plugins + themes | ~700MB | rsync |
|
||||
| wp-content/uploads | 6.5GB | rsync then cleanup (S3 is source of truth) |
|
||||
| wp-content/cache | 4.8GB | **Skip** — regenerates automatically |
|
||||
| w3tc-config | 40KB | **Skip** — W3TC removed |
|
||||
| Database (sql_sibiuindepen) | 9.6GB raw / ~3-4GB compressed dump | mysqldump → import |
|
||||
|
||||
---
|
||||
|
||||
## Cron Inventory
|
||||
|
||||
### Kept and migrated
|
||||
|
||||
| Schedule | Script | Notes |
|
||||
|---|---|---|
|
||||
| `*/5 * * * *` | wp-cron.php trigger | Unchanged |
|
||||
| `0 3 * * *` | cache-warmer.sh | Rewritten for nginx fastcgi_cache |
|
||||
| `0 15 * * *` | cache-warmer.sh | Rewritten for nginx fastcgi_cache |
|
||||
| `0 3 * * 1` | JS asset refresh (OneSignal, FB SDKs) | Unchanged |
|
||||
|
||||
### Dropped
|
||||
|
||||
| Script | Reason |
|
||||
|---|---|
|
||||
| ngxblocker update | Not using ngxblocker |
|
||||
| analyze-nginx-attacks.sh | Panel-specific monitoring |
|
||||
| analyze-php-slow.sh | Panel-specific monitoring |
|
||||
| monitor-php-fpm.sh | Panel-specific monitoring |
|
||||
| memory-cleanup.sh | Linux /proc, wrong server |
|
||||
| AApanel acme_v2.py | Panel SSL renewal |
|
||||
| AApanel site_total_check.py | Panel metric |
|
||||
| AApanel backup.py | Replaced by custom mysqldump script |
|
||||
|
||||
### New on new stack
|
||||
|
||||
| Schedule | Script |
|
||||
|---|---|
|
||||
| `0 2 * * *` | mysqldump → /var/backups + optional S3 upload |
|
||||
| `0 0 * * 0` | ZFS snapshot rotation (keep last 4 weeks) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Preparation (live servers, non-disruptive)
|
||||
|
||||
```sh
|
||||
# Capture configs before touching anything
|
||||
scp -i /root/.ssh/id_ed25519 -P 79 \
|
||||
root@dracula:/www/wwwroot/sibiuindependent.ro/wp-config.php ./wp-config.php.bak
|
||||
|
||||
scp -i /root/.ssh/id_ed25519 -P 79 \
|
||||
root@dracula:/root/cache-warmer.sh ./cache-warmer.sh.original
|
||||
|
||||
# Test dump on transilvan — confirms dump works before migration day
|
||||
ssh -i /root/.ssh/id_ed25519 -p 79 root@transilvan \
|
||||
"mysqldump -S /tmp/mysqld.sock --single-transaction \
|
||||
--routines --triggers --events sql_sibiuindepen \
|
||||
| gzip > /tmp/testdump.sql.gz && ls -lh /tmp/testdump.sql.gz"
|
||||
|
||||
# Record current NPM upstream for rollback reference
|
||||
# Old dracula Tailscale IP: 100.99.157.56
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — DB Node (transilvan-new)
|
||||
|
||||
### 1.1 FreeBSD base + ZFS
|
||||
|
||||
```sh
|
||||
# During FreeBSD 14.x install: enable ZFS, create OS pool on first disk
|
||||
# Add second disk for database isolation after install
|
||||
|
||||
zpool create -o ashift=12 data /dev/da1
|
||||
zfs create -o mountpoint=/var/db/mysql data/mysql
|
||||
zfs set recordsize=16K data/mysql # InnoDB optimal block size
|
||||
zfs set primarycache=metadata data/mysql # InnoDB manages its own buffer pool
|
||||
zfs set compression=lz4 data/mysql # free performance, transparent
|
||||
```
|
||||
|
||||
### 1.2 MySQL 8.4 LTS
|
||||
|
||||
```sh
|
||||
pkg install mysql84-server
|
||||
sysrc mysql_enable=YES
|
||||
sysrc mysql_dbdir=/var/db/mysql
|
||||
```
|
||||
|
||||
`/usr/local/etc/mysql/my.cnf`:
|
||||
```ini
|
||||
[mysqld]
|
||||
bind-address = <transilvan-new Tailscale IP>
|
||||
datadir = /var/db/mysql
|
||||
socket = /tmp/mysql.sock
|
||||
innodb_buffer_pool_size = 24G
|
||||
innodb_log_file_size = 1G
|
||||
innodb_flush_log_at_trx_commit = 1
|
||||
innodb_flush_method = O_DIRECT
|
||||
max_connections = 150
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
skip-name-resolve
|
||||
```
|
||||
|
||||
### 1.3 pf firewall
|
||||
|
||||
`/etc/pf.conf`:
|
||||
```
|
||||
ext_if = "vtnet0"
|
||||
ts_if = "tailscale0"
|
||||
web_ts = "<dracula-new Tailscale IP>"
|
||||
|
||||
set skip on lo0
|
||||
block all
|
||||
pass in on $ts_if proto tcp from $web_ts to any port 3306
|
||||
pass in on $ts_if proto tcp to any port 22
|
||||
pass out all
|
||||
```
|
||||
|
||||
```sh
|
||||
sysrc pf_enable=YES
|
||||
service pf start
|
||||
```
|
||||
|
||||
### 1.4 Tailscale
|
||||
|
||||
```sh
|
||||
pkg install tailscale
|
||||
sysrc tailscaled_enable=YES
|
||||
service tailscaled start
|
||||
tailscale up --hostname=transilvan-new
|
||||
```
|
||||
|
||||
### 1.5 DB restore
|
||||
|
||||
```sh
|
||||
# Dump from old transilvan, import to new
|
||||
# Run from a machine with access to both nodes or pipe via SSH
|
||||
ssh root@transilvan \
|
||||
"mysqldump -S /tmp/mysqld.sock --single-transaction \
|
||||
--routines --triggers --events sql_sibiuindepen" \
|
||||
| ssh root@transilvan-new "mysql -u root sql_sibiuindepen"
|
||||
|
||||
# Create app user (use same password as current DB_PASSWORD in wp-config)
|
||||
mysql -u root -e "
|
||||
CREATE USER 'sql_sibiuindepen'@'<dracula-new Tailscale IP>'
|
||||
IDENTIFIED BY '<password>';
|
||||
GRANT ALL ON sql_sibiuindepen.* TO
|
||||
'sql_sibiuindepen'@'<dracula-new Tailscale IP>';
|
||||
FLUSH PRIVILEGES;"
|
||||
```
|
||||
|
||||
### 1.6 DB backup cron
|
||||
|
||||
`/root/db-backup.sh`:
|
||||
```sh
|
||||
#!/bin/sh
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
DEST=/var/backups
|
||||
mkdir -p $DEST
|
||||
mysqldump -u root --single-transaction \
|
||||
--routines --triggers --events sql_sibiuindepen \
|
||||
| gzip > ${DEST}/db-${DATE}.sql.gz
|
||||
find $DEST -name "*.sql.gz" -mtime +7 -delete
|
||||
```
|
||||
|
||||
Crontab:
|
||||
```
|
||||
0 2 * * * root /root/db-backup.sh
|
||||
0 0 * * 0 root zfs snapshot data/mysql@weekly-$(date +%Y%m%d) && \
|
||||
zfs list -t snapshot -o name | tail -n +6 | xargs -I{} zfs destroy {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Web Node (dracula-new)
|
||||
|
||||
### 2.1 Base packages
|
||||
|
||||
```sh
|
||||
pkg install nginx redis git curl wget
|
||||
sysrc nginx_enable=YES redis_enable=YES
|
||||
service redis start
|
||||
```
|
||||
|
||||
### 2.2 PHP 8.5 from ports
|
||||
|
||||
```sh
|
||||
portsnap fetch extract
|
||||
# Or: git clone https://git.FreeBSD.org/ports.git /usr/ports
|
||||
|
||||
cd /usr/ports/lang/php85
|
||||
make config-recursive
|
||||
# Enable: bcmath curl exif fileinfo gd gettext iconv imagick intl
|
||||
# mbstring opcache pcntl pdo pdo_mysql soap sockets sodium
|
||||
# xml xmlreader xmlwriter xsl zip
|
||||
make install clean
|
||||
|
||||
# Install extensions individually or as group
|
||||
pkg install php85-gd php85-imagick php85-intl php85-mbstring \
|
||||
php85-mysqli php85-pdo_mysql php85-redis php85-soap \
|
||||
php85-sockets php85-sodium php85-xml php85-zip \
|
||||
php85-opcache php85-exif php85-fileinfo php85-bcmath \
|
||||
php85-curl php85-pcntl php85-posix
|
||||
|
||||
pkg install ImageMagick7
|
||||
|
||||
sysrc php_fpm_enable=YES
|
||||
```
|
||||
|
||||
### 2.3 PHP-FPM pool
|
||||
|
||||
`/usr/local/etc/php-fpm.d/sibiuindependent.conf`:
|
||||
```ini
|
||||
[sibiuindependent]
|
||||
user = www
|
||||
group = www
|
||||
listen = /var/run/php-fpm-si.sock
|
||||
listen.owner = www
|
||||
listen.group = www
|
||||
pm = dynamic
|
||||
pm.max_children = 40
|
||||
pm.start_servers = 8
|
||||
pm.min_spare_servers = 4
|
||||
pm.max_spare_servers = 16
|
||||
pm.max_requests = 500
|
||||
```
|
||||
|
||||
`/usr/local/etc/php/php.ini` (relevant):
|
||||
```ini
|
||||
opcache.enable = 1
|
||||
opcache.memory_consumption = 256
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.validate_timestamps = 0
|
||||
opcache.jit = 0
|
||||
session.save_handler = redis
|
||||
session.save_path = "unix:///var/run/redis/redis.sock"
|
||||
```
|
||||
|
||||
### 2.4 nginx with fastcgi_cache
|
||||
|
||||
`/usr/local/etc/nginx/nginx.conf`:
|
||||
```nginx
|
||||
worker_processes auto;
|
||||
events { worker_connections 4096; }
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
server_tokens off;
|
||||
|
||||
fastcgi_cache_path /var/cache/nginx/si
|
||||
levels=1:2
|
||||
keys_zone=si_cache:64m
|
||||
max_size=4g
|
||||
inactive=24h
|
||||
use_temp_path=off;
|
||||
|
||||
fastcgi_cache_key "$scheme$request_method$host$request_uri";
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name sibiuindependent.ro www.sibiuindependent.ro;
|
||||
root /var/www/sibiuindependent.ro;
|
||||
index index.php;
|
||||
|
||||
set $skip_cache 0;
|
||||
if ($request_method = POST) { set $skip_cache 1; }
|
||||
if ($query_string != "") { set $skip_cache 1; }
|
||||
if ($request_uri ~* "/wp-admin/|/wp-login|/xmlrpc|/feed|sitemap") {
|
||||
set $skip_cache 1; }
|
||||
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
|
||||
set $skip_cache 1; }
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php-fpm-si.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
|
||||
fastcgi_cache si_cache;
|
||||
fastcgi_cache_valid 200 301 302 24h;
|
||||
fastcgi_cache_bypass $skip_cache;
|
||||
fastcgi_no_cache $skip_cache;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
mkdir -p /var/cache/nginx/si
|
||||
chown www:www /var/cache/nginx/si
|
||||
service nginx start
|
||||
```
|
||||
|
||||
### 2.5 pf
|
||||
|
||||
`/etc/pf.conf`:
|
||||
```
|
||||
ext_if = "vtnet0"
|
||||
ts_if = "tailscale0"
|
||||
npm_ts = "<NPM node Tailscale IP>"
|
||||
|
||||
set skip on lo0
|
||||
block all
|
||||
pass in on $ts_if proto tcp from $npm_ts to any port 80
|
||||
pass in on $ts_if proto tcp to any port 22
|
||||
pass out all
|
||||
```
|
||||
|
||||
```sh
|
||||
sysrc pf_enable=YES
|
||||
service pf start
|
||||
```
|
||||
|
||||
### 2.6 Tailscale
|
||||
|
||||
```sh
|
||||
pkg install tailscale
|
||||
sysrc tailscaled_enable=YES
|
||||
service tailscaled start
|
||||
tailscale up --hostname=dracula-new
|
||||
```
|
||||
|
||||
### 2.7 rsync web files
|
||||
|
||||
```sh
|
||||
# Exclude cache — does not migrate, regenerates on first request
|
||||
rsync -avz --progress \
|
||||
-e "ssh -i /root/.ssh/id_ed25519 -p 79" \
|
||||
--exclude="wp-content/cache/" \
|
||||
--exclude="wp-content/w3tc-config/" \
|
||||
root@dracula:/www/wwwroot/sibiuindependent.ro/ \
|
||||
/var/www/sibiuindependent.ro/
|
||||
|
||||
chown -R www:www /var/www/sibiuindependent.ro
|
||||
```
|
||||
|
||||
### 2.8 wp-config.php changes
|
||||
|
||||
Two changes on the new node only:
|
||||
|
||||
```php
|
||||
// 1. Update DB_HOST to new transilvan Tailscale IP
|
||||
define( 'DB_HOST', '<transilvan-new Tailscale IP>' );
|
||||
|
||||
// 2. WP_CACHE stays true for Redis Object Cache drop-in
|
||||
// Remove the W3TC comment, keep the constant:
|
||||
define( 'WP_CACHE', true );
|
||||
```
|
||||
|
||||
Remove W3TC plugin and its drop-ins:
|
||||
```sh
|
||||
rm -rf /var/www/sibiuindependent.ro/wp-content/plugins/w3-total-cache
|
||||
rm -f /var/www/sibiuindependent.ro/wp-content/advanced-cache.php
|
||||
rm -f /var/www/sibiuindependent.ro/wp-content/object-cache.php
|
||||
```
|
||||
|
||||
Install Redis Object Cache plugin (Till Krüss), activate, configure:
|
||||
```php
|
||||
// In wp-config.php, before "That's all, stop editing":
|
||||
define( 'WP_REDIS_PATH', '/var/run/redis/redis.sock' );
|
||||
define( 'WP_REDIS_SCHEME', 'unix' );
|
||||
```
|
||||
|
||||
### 2.9 Crons
|
||||
|
||||
```
|
||||
*/5 * * * * www curl -s -o /dev/null https://sibiuindependent.ro/wp-cron.php?doing_wp_cron
|
||||
0 3 * * * root /root/cache-warmer.sh >> /root/cache-warmer.log 2>&1
|
||||
0 15 * * * root /root/cache-warmer.sh >> /root/cache-warmer.log 2>&1
|
||||
0 3 * * 1 root /root/js-refresh.sh >> /root/js-refresh.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Warmer (rewritten for nginx fastcgi_cache)
|
||||
|
||||
`/root/cache-warmer.sh`:
|
||||
```sh
|
||||
#!/bin/sh
|
||||
# Incremental cache warmer for nginx fastcgi_cache.
|
||||
# Checks X-Cache-Status header: skips HITs, warms MISSes.
|
||||
# Requires curl >= 7.84 (FreeBSD 14 ships curl 8.x — compatible).
|
||||
|
||||
SITE='https://sibiuindependent.ro'
|
||||
CONCURRENCY=4
|
||||
LOG='/root/cache-warmer.log'
|
||||
URL_FILE='/tmp/cache-warmer-urls.txt'
|
||||
DONE_FILE='/tmp/cache-warmer-done.txt'
|
||||
|
||||
echo "[$(date)] Starting cache warmer (incremental, nginx fastcgi_cache)" | tee -a "$LOG"
|
||||
|
||||
: > "$URL_FILE"
|
||||
SITEMAPS=$(curl -sk "${SITE}/sitemap_index.xml" | \
|
||||
grep -oE 'https?://[^<]+\.xml' | grep -v image)
|
||||
echo "[$(date)] Sub-sitemaps: $(echo "$SITEMAPS" | wc -l)" | tee -a "$LOG"
|
||||
|
||||
for SMAP in $SITEMAPS; do
|
||||
curl -sk "$SMAP" | grep -oE 'https?://[^<]+' | grep -v '\.xml' >> "$URL_FILE"
|
||||
sleep 0.3
|
||||
done
|
||||
|
||||
TOTAL=$(wc -l < "$URL_FILE")
|
||||
echo "[$(date)] Total URLs from sitemap: $TOTAL" | tee -a "$LOG"
|
||||
|
||||
: > "$DONE_FILE"
|
||||
WARMED=0
|
||||
SKIPPED=0
|
||||
COUNT=0
|
||||
|
||||
while IFS= read -r URL; do
|
||||
(
|
||||
RESULT=$(curl -sk -o /dev/null \
|
||||
-w '%{http_code} %header{x-cache-status}' \
|
||||
-A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' \
|
||||
-H 'Accept-Encoding: gzip' \
|
||||
--max-time 20 "$URL")
|
||||
echo "$RESULT $URL" >> "$DONE_FILE"
|
||||
) &
|
||||
COUNT=$((COUNT + 1))
|
||||
if [ $((COUNT % CONCURRENCY)) -eq 0 ]; then
|
||||
wait
|
||||
fi
|
||||
sleep 0.05
|
||||
done < "$URL_FILE"
|
||||
wait
|
||||
|
||||
WARMED=$(grep -vc 'HIT' "$DONE_FILE" 2>/dev/null || echo 0)
|
||||
SKIPPED=$(grep -c 'HIT' "$DONE_FILE" 2>/dev/null || echo 0)
|
||||
echo "[$(date)] Done. Warmed: $WARMED, Skipped (HIT): $SKIPPED" | tee -a "$LOG"
|
||||
```
|
||||
|
||||
### Key difference from original
|
||||
|
||||
| | Original (W3TC) | New (nginx fastcgi_cache) |
|
||||
|---|---|---|
|
||||
| Cache check method | Stat filesystem for `_index_slash_ssl.html` | HTTP `X-Cache-Status: HIT` header |
|
||||
| Cache path | `/www/wwwroot/.../cache/page_enhanced/...` | `/var/cache/nginx/si/` (hashed, not human-readable) |
|
||||
| Logic | Skip if file exists | Skip if header is `HIT` |
|
||||
| Concurrency | Background subshells | Background subshells (unchanged) |
|
||||
| Sitemap parsing | Unchanged | Unchanged |
|
||||
|
||||
---
|
||||
|
||||
## JS Asset Refresh Cron
|
||||
|
||||
`/root/js-refresh.sh`:
|
||||
```sh
|
||||
#!/bin/sh
|
||||
# Weekly refresh of self-hosted third-party JS assets
|
||||
WEBROOT=/var/www/sibiuindependent.ro
|
||||
|
||||
curl -s https://cdn.onesignal.com/sdks/OneSignalSDK.js \
|
||||
| sed 's|//# sourceMappingURL=.*||' \
|
||||
> ${WEBROOT}/onesignal.js
|
||||
|
||||
curl -s https://cdn.onesignal.com/sdks/OneSignalPageSDKES6.js \
|
||||
| sed 's|//# sourceMappingURL=.*||' \
|
||||
> ${WEBROOT}/onesignal-es6.js
|
||||
|
||||
curl -s https://connect.facebook.net/en_US/sdk.js > ${WEBROOT}/fb-sdk.js
|
||||
curl -s https://connect.facebook.net/ro_RO/sdk.js > ${WEBROOT}/fb-sdk-ro.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Zero-downtime Cutover
|
||||
|
||||
### Pre-cutover checklist (run on dracula-new before touching NPM)
|
||||
|
||||
```sh
|
||||
# 1. Site responds via new stack directly
|
||||
curl -sk -o /dev/null -w "%{http_code}" \
|
||||
http://<dracula-new Tailscale IP>/ \
|
||||
-H "Host: sibiuindependent.ro"
|
||||
# Expected: 200
|
||||
|
||||
# 2. DB connection live
|
||||
mysql -h <transilvan-new Tailscale IP> \
|
||||
-u sql_sibiuindepen -p sql_sibiuindepen \
|
||||
-e "SELECT COUNT(*) FROM wp_posts WHERE post_status='publish';"
|
||||
|
||||
# 3. Redis object cache connected
|
||||
wp --path=/var/www/sibiuindependent.ro --allow-root redis-cache status
|
||||
|
||||
# 4. PHP extensions present
|
||||
php85 -m | grep -E "imagick|redis|pdo_mysql|opcache|intl|soap|sodium"
|
||||
|
||||
# 5. nginx fastcgi_cache working — second request should be HIT
|
||||
curl -sI http://<dracula-new Tailscale IP>/ -H "Host: sibiuindependent.ro" \
|
||||
| grep X-Cache-Status
|
||||
# First request: MISS, second request: HIT
|
||||
```
|
||||
|
||||
### Cutover sequence
|
||||
|
||||
```
|
||||
T-0 Disable cache warmer crons on OLD dracula (comment out both lines)
|
||||
|
||||
T-1 Final rsync of uploads (incremental delta only):
|
||||
rsync -avz --progress \
|
||||
-e "ssh -i /root/.ssh/id_ed25519 -p 79" \
|
||||
--exclude="wp-content/cache/" \
|
||||
root@dracula:/www/wwwroot/sibiuindependent.ro/wp-content/uploads/ \
|
||||
/var/www/sibiuindependent.ro/wp-content/uploads/
|
||||
|
||||
T-2 Final DB dump and import:
|
||||
ssh root@transilvan \
|
||||
"mysqldump -S /tmp/mysqld.sock --single-transaction \
|
||||
--routines --triggers --events sql_sibiuindepen" \
|
||||
| ssh root@transilvan-new "mysql -u root sql_sibiuindepen"
|
||||
|
||||
T-3 Switch NPM upstream:
|
||||
old: 100.99.157.56 (dracula Tailscale IP)
|
||||
new: <dracula-new Tailscale IP>
|
||||
(single upstream change in NPM — no DNS TTL involved, instant)
|
||||
|
||||
T-4 Smoke test:
|
||||
- Homepage loads
|
||||
- An article page loads
|
||||
- wp-admin accessible
|
||||
- Second page load shows X-Cache-Status: HIT
|
||||
|
||||
T-5 Enable cache warmer on new dracula
|
||||
```
|
||||
|
||||
**Rollback:** Revert NPM upstream to `100.99.157.56`. Old stack is untouched throughout. No DNS change required. Instant.
|
||||
|
||||
**Old VMs:** Keep powered on (no traffic) for 48 hours. Decommission after confirmed stable.
|
||||
|
||||
### Post-cutover
|
||||
|
||||
```sh
|
||||
# Run cache warmer immediately on new node
|
||||
bash /root/cache-warmer.sh
|
||||
|
||||
# Monitor
|
||||
tail -f /var/log/nginx/error.log
|
||||
tail -f /var/log/php-fpm.log
|
||||
|
||||
# ZFS snapshot of DB post-migration
|
||||
zfs snapshot data/mysql@post-migration-$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Items at Time of Planning
|
||||
|
||||
- [ ] Confirm new Tailscale hostnames for both nodes (determines pf rules and wp-config DB_HOST)
|
||||
- [ ] Confirm NPM node Tailscale IP (determines pf rule on dracula-new)
|
||||
- [ ] PHP 8.5 from ports — attempt first, fall back to pkg php84 if dependency issue
|
||||
- [ ] Redis Object Cache plugin — already in plugins dir or fresh install needed
|
||||
- [ ] W3TC features in use beyond caching: minify? If yes, switch to standalone nginx-based minification or keep W3TC minify only with nginx page cache
|
||||
- [ ] ZFS pool disk — confirm second disk available on transilvan-new VM
|
||||
- [ ] MySQL 8.4 vs MariaDB 11.4 LTS — plan targets MySQL 8.4; revisit if preferred
|
||||
- [ ] Beszel and Telegraf — confirm if keeping on new stack (both have FreeBSD builds)
|
||||
Reference in New Issue
Block a user