feat: complete decebal deployment, update README with actual IPs and configs

- decebal fully deployed: MySQL 8.4.9 running, app user created
  (sql_sibiuindepen@100.115.128.41), backup cron installed
- ZFS pool 'data' ONLINE on /dev/da1, data/mysql at /var/db/mysql
- pf active: 3306 allowed from 100.115.128.41 only
- zamolxis -> decebal MySQL connectivity verified
- README: replace all Tailscale IP placeholders with actuals
  (zamolxis 100.115.128.41, decebal 100.67.166.29)
- README: fix PHP extension package names (pecl-imagick, pecl-redis;
  opcache bundled in base php85)
- README: fix nginx if operator (no != support in nginx if blocks)
- README: update my.cnf to deployed config (innodb_redo_log_capacity,
  innodb_buffer_pool_instances, slow query log, sql_mode)
- README: update pf configs to deployed rulesets (both nodes)
- README: update backup script to deployed version with .my.cnf auth
- README: add deployment status table, resolve open items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Malin
2026-05-29 10:19:38 +02:00
parent 795e84d438
commit 288b79d8eb

261
README.md
View File

@@ -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 = <decebal Tailscale IP>
# 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 = "<zamolxis Tailscale IP>"
# 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'@'<zamolxis Tailscale IP>'
IDENTIFIED BY '<password>';
GRANT ALL ON sql_sibiuindepen.* TO
'sql_sibiuindepen'@'<zamolxis Tailscale IP>';
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 = "<NPM node Tailscale IP>"
# 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', '<decebal Tailscale IP>' );
// 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://<zamolxis Tailscale IP>/ \
http://100.115.128.41/ \
-H "Host: sibiuindependent.ro"
# Expected: 200
# 2. DB connection live
mysql -h <decebal Tailscale IP> \
-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://<zamolxis Tailscale IP>/ -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: <zamolxis Tailscale IP>
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 50150MB 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