diff --git a/README.md b/README.md index 44659d9..8c38811 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Sibiu Independent — Stack Migration ## May 2026 -Migration of `sibiuindependent.ro` from Debian 12 + AApanel to FreeBSD 14.x minimal stack. +Migration of `sibiuindependent.ro` from Debian 12 + AApanel to FreeBSD 15.0 minimal stack. --- ## Current Architecture ``` -[internet] → [Nginx Proxy Manager] → [dracula, Tailscale] → [transilvan, Tailscale] +[internet] → [Nginx Proxy Manager] → [dracula 100.99.157.56, 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 | +| Node | Role | OS | Specs | Tailscale IP | +|---|---|---|---|---| +| dracula | Web | Debian 12 | 16 vCPU, 32GB RAM, 245GB disk | 100.99.157.56 | +| 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. @@ -123,20 +123,20 @@ FreeBSD 15.0 over 14.x specifically because PHP 8.5 is a **binary package** on 1 ## Target Architecture ``` -[internet] → [NPM node, Tailscale] → [zamolxis FreeBSD 15.0, Tailscale] → [decebal FreeBSD 15.0, Tailscale] +[internet] → [NPM node, Tailscale] → [zamolxis 100.115.128.41, Tailscale] → [decebal 100.67.166.29, Tailscale] zamolxis (FreeBSD 15.0) decebal (FreeBSD 15.0) ────────────────────────── ───────────────────────────── -nginx (pkg) ZFS pool → /var/db/mysql -PHP 8.5.6 (pkg) 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 Proxmox native monitoring -Tailscale +nginx 1.30.2 (pkg) ZFS pool "data" → /var/db/mysql +PHP 8.5.4 (pkg) recordsize=16K (InnoDB optimal) + gd, pecl-imagick, intl, mbstring, compression=lz4 + opcache (bundled), pdo_mysql, primarycache=metadata + pecl-redis, soap, sockets, MySQL 8.4.9 LTS (pkg) + sodium, xml, zip, exif, fileinfo pf: 3306 from 100.115.128.41 only +nginx fastcgi_cache (filesystem) Tailscale 100.67.166.29 +Redis 8.6.2 (pkg, unix socket) SSH on Tailscale only +pf: 80 from Tailscale only Proxmox native monitoring +Tailscale 100.115.128.41 SSH on Tailscale only Proxmox native monitoring ``` @@ -421,8 +421,8 @@ ssh -i /root/.ssh/id_ed25519 -p 79 root@transilvan \ ### 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 +# During FreeBSD 15.0 install: enable ZFS, create OS pool on first disk (da0) +# da1 is left raw for the database ZFS pool zpool create -o ashift=12 data /dev/da1 zfs create -o mountpoint=/var/db/mysql data/mysql @@ -444,32 +444,80 @@ sysrc mysql_dbdir=/var/db/mysql `/usr/local/etc/mysql/my.cnf`: ```ini [mysqld] -bind-address = +# Network +bind-address = 100.67.166.29 +port = 3306 +mysqlx = 0 + +# Paths datadir = /var/db/mysql -socket = /tmp/mysql.sock +socket = /var/db/mysql/mysql.sock +log_error = /var/db/mysql/decebal.err +pid-file = /var/db/mysql/decebal.pid + +# InnoDB innodb_buffer_pool_size = 26G -innodb_log_file_size = 1G +innodb_buffer_pool_instances = 8 +innodb_redo_log_capacity = 2G innodb_flush_log_at_trx_commit = 1 innodb_flush_method = O_DIRECT +innodb_io_capacity = 2000 +innodb_io_capacity_max = 4000 + +# Connections max_connections = 150 +thread_cache_size = 16 +table_open_cache = 4000 + +# Character set character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci + +# Logging +slow_query_log = 1 +slow_query_log_file = /var/db/mysql/slow.log +long_query_time = 2 +binlog_expire_logs_seconds = 259200 + +# Safety skip-name-resolve +sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION + +[client] +socket = /var/db/mysql/mysql.sock ``` +Note: `innodb_redo_log_capacity` replaces the deprecated `innodb_log_file_size` in MySQL 8.4. + ### 1.3 pf firewall `/etc/pf.conf`: ``` +# decebal — database server pf ruleset ext_if = "vtnet0" ts_if = "tailscale0" -web_ts = "" + +# Management: local subnet, Tailscale, allowed external IPs +mgmt = "{ 127.0.0.0/8, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, 194.56.239.153, 109.69.48.0/24 }" + +# zamolxis Tailscale IP — only source allowed to reach MySQL +zamolxis_ts = "100.115.128.41" 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 +set block-policy drop +scrub in all + +block in all +block out quick inet6 all + +pass out all keep state + +# SSH — management IPs on either interface +pass in on $ext_if proto tcp from $mgmt to any port 22 keep state +pass in on $ts_if proto tcp to any port 22 keep state + +# MySQL — zamolxis Tailscale IP only, via Tailscale interface +pass in on $ts_if proto tcp from $zamolxis_ts to any port 3306 keep state ``` ```sh @@ -489,20 +537,16 @@ tailscale up --hostname=decebal ### 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 \ +# Dump from old transilvan (port 79), import to new decebal +# Run from a machine with access to both nodes +ssh -p 79 root@transilvan \ "mysqldump -S /tmp/mysqld.sock --single-transaction \ --routines --triggers --events sql_sibiuindepen" \ - | ssh root@decebal "mysql -u root sql_sibiuindepen" + | ssh root@100.67.166.29 \ + "mysql --socket=/var/db/mysql/mysql.sock sql_sibiuindepen" -# Create app user (use same password as current DB_PASSWORD in wp-config) -mysql -u root -e " - CREATE USER 'sql_sibiuindepen'@'' - IDENTIFIED BY ''; - GRANT ALL ON sql_sibiuindepen.* TO - 'sql_sibiuindepen'@''; - FLUSH PRIVILEGES;" +# App user already created — zamolxis Tailscale IP 100.115.128.41 +# Credentials: sql_sibiuindepen / see keepass ``` ### 1.6 DB backup cron @@ -510,20 +554,47 @@ mysql -u root -e " `/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 +# Daily MySQL backup for decebal +# Retention: 7 days local. Credentials via /root/.my.cnf +BACKUP_DIR="/var/db/backups/mysql" +DB="sql_sibiuindepen" +DATE=$(date +%Y%m%d_%H%M%S) +LOG="/var/log/db-backup.log" + +mkdir -p "$BACKUP_DIR" +echo "[$(date)] Starting backup of $DB" >> "$LOG" + +/usr/local/bin/mysqldump \ + --socket=/var/db/mysql/mysql.sock \ + --single-transaction \ + --quick \ + --routines \ + --triggers \ + --set-gtid-purged=OFF \ + "$DB" | gzip > "${BACKUP_DIR}/${DB}_${DATE}.sql.gz" + +if [ $? -eq 0 ]; then + SIZE=$(du -sh "${BACKUP_DIR}/${DB}_${DATE}.sql.gz" | cut -f1) + echo "[$(date)] Backup OK: ${DB}_${DATE}.sql.gz ($SIZE)" >> "$LOG" +else + echo "[$(date)] BACKUP FAILED for $DB" >> "$LOG" + exit 1 +fi + +find "$BACKUP_DIR" -name "${DB}_*.sql.gz" -mtime +7 -delete +echo "[$(date)] Pruned backups older than 7 days" >> "$LOG" ``` -Crontab: +Crontab (root, on decebal): ``` -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 {} +# Daily MySQL dump at 02:00 +0 2 * * * root /root/db-backup.sh + +# Weekly ZFS snapshot (Sunday 00:30) +30 0 * * 0 root zfs snapshot data/mysql@weekly-$(date +\%Y\%m\%d) + +# Prune ZFS snapshots — keep last 4 weeks (Sunday 01:00) +0 1 * * 0 root zfs list -H -t snapshot -o name data/mysql | sort | head -n -4 | xargs -r zfs destroy ``` --- @@ -545,25 +616,25 @@ service redis start On FreeBSD 15.0, PHP 8.5 and all extensions are available as binary packages — no ports compilation required. ```sh -# Base PHP 8.5 + FPM -pkg install -y php85 php85-extensions +# Base PHP 8.5 + FPM (opcache is bundled in the base php85 package) +pkg install -y php85 # Extensions needed for WordPress +# Note: imagick and redis are PECL extensions — package prefix is php85-pecl-* pkg install -y \ php85-bcmath \ php85-curl \ php85-exif \ php85-fileinfo \ php85-gd \ - php85-imagick \ + php85-pecl-imagick \ php85-intl \ php85-mbstring \ php85-mysqli \ - php85-opcache \ php85-pcntl \ php85-pdo_mysql \ php85-posix \ - php85-redis \ + php85-pecl-redis \ php85-soap \ php85-sockets \ php85-sodium \ @@ -639,7 +710,7 @@ http { set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; } - if ($query_string != "") { set $skip_cache 1; } + if ($query_string) { set $skip_cache 1; } # nginx if does not support != 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") { @@ -681,17 +752,35 @@ service nginx start `/etc/pf.conf`: ``` +# zamolxis — web server pf ruleset ext_if = "vtnet0" ts_if = "tailscale0" -npm_ts = "" + +# Management: local subnet, Tailscale, allowed external IPs +mgmt = "{ 127.0.0.0/8, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, 194.56.239.153, 109.69.48.0/24 }" 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 +set block-policy drop +scrub in all + +# Default deny +block in all +block out quick inet6 all + +# Allow all outbound (package updates, S3, SMTP relay, etc.) +pass out all keep state + +# SSH — from management IPs on either interface +pass in on $ext_if proto tcp from $mgmt to any port 22 keep state +pass in on $ts_if proto tcp to any port 22 keep state + +# HTTP — from NPM via Tailscale only +# Tighten to specific NPM Tailscale IP once known +pass in on $ts_if proto tcp to any port 80 keep state ``` +Note: tighten the HTTP rule to NPM's specific Tailscale IP once confirmed. + ```sh sysrc pf_enable=YES service pf start @@ -725,8 +814,8 @@ chown -R www:www /var/www/sibiuindependent.ro Two changes on the new node only: ```php -// 1. Update DB_HOST to new transilvan Tailscale IP -define( 'DB_HOST', '' ); +// 1. Update DB_HOST to decebal Tailscale IP +define( 'DB_HOST', '100.67.166.29' ); // 2. WP_CACHE stays true for Redis Object Cache drop-in // Remove the W3TC comment, keep the constant: @@ -856,13 +945,14 @@ curl -s https://connect.facebook.net/ro_RO/sdk.js > ${WEBROOT}/fb-sdk-ro.js ```sh # 1. Site responds via new stack directly curl -sk -o /dev/null -w "%{http_code}" \ - http:/// \ + http://100.115.128.41/ \ -H "Host: sibiuindependent.ro" # Expected: 200 # 2. DB connection live -mysql -h \ - -u sql_sibiuindepen -p sql_sibiuindepen \ +mysql -h 100.67.166.29 \ + -u sql_sibiuindepen -p \ + --get-server-public-key \ -e "SELECT COUNT(*) FROM wp_posts WHERE post_status='publish';" # 3. Redis object cache connected @@ -872,7 +962,7 @@ wp --path=/var/www/sibiuindependent.ro --allow-root redis-cache status 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:/// -H "Host: sibiuindependent.ro" \ +curl -sI http://100.115.128.41/ -H "Host: sibiuindependent.ro" \ | grep X-Cache-Status # First request: MISS, second request: HIT ``` @@ -897,7 +987,7 @@ T-2 Final DB dump and import: T-3 Switch NPM upstream: old: 100.99.157.56 (dracula Tailscale IP) - new: + new: 100.115.128.41 (zamolxis Tailscale IP) (single upstream change in NPM — no DNS TTL involved, instant) T-4 Smoke test: @@ -929,14 +1019,31 @@ zfs snapshot data/mysql@post-migration-$(date +%Y%m%d) --- -## Open Items at Time of Planning +## Open Items -- [ ] Confirm new Tailscale hostnames for both nodes (determines pf rules and wp-config DB_HOST) -- [ ] Confirm NPM node Tailscale IP (determines pf rule on zamolxis) -- [x] PHP 8.5 — binary package on FreeBSD 15.0, `pkg install php85`. No compilation. All extensions confirmed available. -- [ ] 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 decebal VM -- [ ] MySQL 8.4 vs MariaDB 11.4 LTS — plan targets MySQL 8.4; revisit if preferred -- [x] Beszel and Telegraf — dropped. Proxmox native VM monitoring is sufficient. -- [x] Redis placement — local to zamolxis (unix socket). No 3rd host. WordPress object cache uses 50–150MB RAM; a dedicated VM would cost more resources than Redis itself consumes. A separate Redis host is only justified when scaling to multiple web nodes sharing a single cache. +- [x] Tailscale IPs confirmed — zamolxis: 100.115.128.41 / decebal: 100.67.166.29 +- [ ] NPM node Tailscale IP — tighten zamolxis pf HTTP rule to specific NPM IP once confirmed +- [x] PHP 8.5 — binary package on FreeBSD 15.0, all extensions installed. PECL packages use `php85-pecl-*` prefix (imagick, redis). opcache bundled in base php85. +- [ ] Redis Object Cache plugin — confirm if already in wp-content/plugins from old dracula or install fresh +- [ ] W3TC minify in use? — if yes, decide before cutover: drop minify or use separate plugin +- [x] ZFS pool — data pool on /dev/da1 (100GB), ONLINE. data/mysql dataset ONLINE at /var/db/mysql +- [x] MySQL 8.4.9 LTS — deployed and running on decebal +- [x] Beszel and Telegraf — dropped. Proxmox native monitoring sufficient. +- [x] Redis placement — local to zamolxis (unix socket). No 3rd host needed. +- [ ] Phase 2 — rsync WP files from dracula (port 79) to zamolxis /var/www/sibiuindependent.ro +- [ ] Phase 2 — update wp-config.php with DB_HOST=100.67.166.29 and Redis constants +- [ ] Phase 2 — remove W3TC, install Redis Object Cache plugin +- [ ] Phase 3 — pre-cutover checklist verification +- [ ] Phase 3 — NPM upstream switch 100.99.157.56 → 100.115.128.41 + +## Deployment Status (as of 2026-05-29) + +| Node | Status | Notes | +|---|---|---| +| zamolxis | **Deployed** | nginx 1.30.2, PHP 8.5.4, Redis 8.6.2, pf active, crons installed | +| decebal | **Deployed** | MySQL 8.4.9, ZFS pool online, pf active, app user created, backup cron installed | + +### Connectivity verified +- zamolxis → decebal MySQL (100.67.166.29:3306) as `sql_sibiuindepen`: OK +- pf on decebal allows only 100.115.128.41 to reach port 3306: OK +- Backup test run: /var/db/backups/mysql/sql_sibiuindepen_*.sql.gz created OK