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:
261
README.md
261
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 = <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 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
|
||||
|
||||
Reference in New Issue
Block a user