2026-05-29 07:45:10 +02:00
# Sibiu Independent — Stack Migration
## May 2026
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>
2026-05-29 10:19:38 +02:00
Migration of `sibiuindependent.ro` from Debian 12 + AApanel to FreeBSD 15.0 minimal stack.
2026-05-29 07:45:10 +02:00
---
## Current Architecture
```
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>
2026-05-29 10:19:38 +02:00
[internet] → [Nginx Proxy Manager] → [dracula 100.99.157.56, Tailscale] → [transilvan, Tailscale]
2026-05-29 07:45:10 +02:00
```
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>
2026-05-29 10:19:38 +02:00
| 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 | — |
2026-05-29 07:45:10 +02:00
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 |
2026-05-29 08:13:55 +02:00
| Monitoring | Telegraf, Beszel agent, Wazuh agent, Five Nines agent (all dropped in migration) |
2026-05-29 07:45:10 +02:00
| 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/ |
2026-05-29 08:13:55 +02:00
| Monitoring | Telegraf, Beszel agent, Wazuh agent, Five Nines agent (all dropped in migration) |
2026-05-29 07:45:10 +02:00
| 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)
2026-05-29 08:25:02 +02:00
### Decision: **FreeBSD 15.0 on both nodes**
2026-05-29 07:45:10 +02:00
2026-05-29 08:25:02 +02:00
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.
FreeBSD 15.0 over 14.x specifically because PHP 8.5 is a **binary package ** on 15.0 — `pkg install php85` and all extensions install directly, no ports compilation required. FreeBSD 15.0 was released December 2, 2025 and has 6 months of production use. MySQL 8.4 LTS, Redis 8.x, and all required PHP extensions are in the binary package tree. pf is simpler and more powerful than nftables. Tailscale has official FreeBSD support.
---
## Target Stack Versions
| Component | Version | Source | Notes |
|---|---|---|---|
| OS | FreeBSD 15.0 | Release Dec 2, 2025 | Production Release, 6 months in production |
| nginx | 1.30.x | `pkg install nginx` | |
| PHP | 8.5.6 | `pkg install php85` | Binary pkg on FreeBSD 15 — no compilation |
| MySQL | 8.4.9 LTS | `pkg install mysql84-server` | LTS branch. 9.0 and 9.1 both EOL and removed from ports |
| Redis | 8.6.3 | `pkg install redis` | Redis 8.x is current major version |
| Tailscale | latest | `pkg install tailscale` | Official FreeBSD package |
2026-05-29 07:45:10 +02:00
---
## Target Architecture
```
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>
2026-05-29 10:19:38 +02:00
[internet] → [NPM node, Tailscale] → [zamolxis 100.115.128.41, Tailscale] → [decebal 100.67.166.29, Tailscale]
2026-05-29 07:45:10 +02:00
2026-05-29 08:45:19 +02:00
zamolxis (FreeBSD 15.0) decebal (FreeBSD 15.0)
2026-05-29 07:45:10 +02:00
────────────────────────── ─────────────────────────────
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>
2026-05-29 10:19:38 +02:00
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
2026-05-29 07:45:10 +02:00
SSH on Tailscale only
2026-05-29 08:13:55 +02:00
Proxmox native monitoring
2026-05-29 07:45:10 +02:00
```
### 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 |
2026-05-29 08:13:55 +02:00
| Wazuh agent | Not required |
| Five Nines agent | Not required |
| Beszel agent | Not required — Proxmox native monitoring covers VM metrics |
| Telegraf | Not required — Proxmox native monitoring covers VM metrics |
2026-05-29 07:45:10 +02:00
| 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 |
---
2026-05-29 08:00:52 +02:00
## Host Hardware
2026-05-29 08:13:55 +02:00
| Host | CPU | Logical CPUs | RAM | Storage | Available to VMs | VM |
|---|---|---|---|---|---|---|
2026-05-29 08:45:19 +02:00
| dracula host | Xeon E5-1620 v2 @ 3.70GHz | 8 (4 cores + HT) | 64GB | Datacenter HDD | ~full | zamolxis / ID 900 (web) |
| transilvan host | Xeon E5-1620 v2 @ 3.70GHz | 8 (4 cores + HT) | 32GB | SSD 160GB (~130GB free) | 130GB | decebal / ID 901 (db) — sole VM |
2026-05-29 08:00:52 +02:00
2026-05-29 08:45:19 +02:00
Both hosts run Proxmox VE (confirmed via qemu-guest-agent on current VMs). Decebal's host is dedicated — no other VMs share it.
2026-05-29 08:00:52 +02:00
---
## VM Sizing
### Rationale for 200 concurrent users
With nginx fastcgi_cache, the vast majority of requests never invoke PHP. On a news site, traffic concentrates on recent articles — the hot working set is small and stays warm in FreeBSD's unified buffer cache (UBC) regardless of underlying disk speed.
| Metric | Value | Notes |
|---|---|---|
| Expected cache HIT rate | ~85– 90% | News sites have concentrated traffic on recent content |
| Concurrent PHP requests at peak | ~20– 30 | 200 users × ~15% miss rate |
| PHP-FPM workers (pm.max_children) | 40 | Headroom above expected peak |
| RAM per PHP-FPM worker | ~35MB RSS | WordPress with active plugins |
| PHP-FPM peak RAM | ~1.4GB | 40 workers × 35MB |
| nginx HIT response time | <5ms | Served from UBC, no PHP involved |
| nginx MISS response time | 100– 400ms | WP query + render |
2026-05-29 08:45:19 +02:00
| InnoDB buffer pool hit rate | >99% | 26GB pool, 9.6GB dataset fits entirely in RAM with 16GB to spare |
2026-05-29 08:00:52 +02:00
| Active DB connections | ≤ 40 | One per active PHP-FPM worker |
2026-05-29 08:45:19 +02:00
### zamolxis (web VM on HDD host)
2026-05-29 08:00:52 +02:00
| Parameter | Value |
|---|---|
| vCPU | 6 |
2026-05-29 08:45:19 +02:00
| RAM | 24GB |
2026-05-29 08:00:52 +02:00
| Disk | 80GB single virtual disk (HDD-backed) |
| Balloon | Enabled (web VM, acceptable) |
2026-05-29 08:45:19 +02:00
RAM breakdown: PHP-FPM peak 1.4GB + Redis ~150MB + nginx + OS ~1GB + * * ~20GB available for UBC page cache**. With 20GB of UBC headroom, the entire nginx fastcgi_cache working set lives in RAM regardless of what is on disk — HDD latency becomes irrelevant for cached pages. Cold cache startup and log writes (sequential) are the only HDD operations that matter.
2026-05-29 08:00:52 +02:00
2026-05-29 08:45:19 +02:00
### decebal (DB VM on SSD host — sole VM)
2026-05-29 08:00:52 +02:00
| Parameter | Value |
|---|---|
2026-05-29 08:45:19 +02:00
| vCPU | 6 |
| RAM | 30GB |
2026-05-29 08:13:55 +02:00
| Disk 1 — OS | 20GB virtual (SSD-backed) |
| Disk 2 — ZFS data pool | 100GB virtual (SSD-backed) |
| Total disk | 120GB — fits within 130GB available, 10GB buffer for Proxmox overhead |
2026-05-29 08:00:52 +02:00
| Balloon | **Disabled ** — memory ballooning must not reclaim InnoDB buffer pool RAM |
2026-05-29 08:45:19 +02:00
RAM breakdown: innodb_buffer_pool_size 26GB + MySQL overhead ~2GB + OS ~2GB. Decebal is the sole VM on its host — 30GB allocation leaves 2GB for the Proxmox host OS, which is the practical minimum.
6 vCPU (from 8 logical CPUs on host) gives MySQL adequate threads for InnoDB background I/O, parallel queries, and connection handling while leaving 2 threads for Proxmox.
2026-05-29 08:00:52 +02:00
2026-05-29 08:13:55 +02:00
**Disk sizing rationale:** FreeBSD base installs to ~2GB; 20GB for the OS disk is generous for binaries, ports tree fragments, and logs. The 100GB ZFS data pool provides: 9.6GB current dataset (likely 6– 7GB after lz4 compression), 7 days of compressed mysqldumps (~4GB each = ~28GB), binlog rotation buffer (expire_logs_days = 3), and substantial growth headroom at the current data trajectory.
Both virtual disks originate from the same physical SSD pool in Proxmox. Separation is logical: independent sizing, clean `zpool create data /dev/da1` , ZFS snapshots on the data disk without touching the OS disk, and straightforward future migration if a dedicated physical disk is added to the host.
2026-05-29 08:00:52 +02:00
---
## VM Provisioning (Proxmox)
2026-05-29 08:45:19 +02:00
### 1. Download FreeBSD 15.0 ISO on each Proxmox host
2026-05-29 08:00:52 +02:00
```sh
wget -P /var/lib/vz/template/iso/ \
2026-05-29 08:25:02 +02:00
https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/15.0/FreeBSD-15.0-RELEASE-amd64-disc1.iso
2026-05-29 08:00:52 +02:00
```
2026-05-29 08:45:19 +02:00
### 2. Create zamolxis / ID 900 (run on the HDD host)
2026-05-29 08:00:52 +02:00
Via CLI:
```sh
2026-05-29 08:45:19 +02:00
qm create 900 \
--name zamolxis \
--memory 24576 \
--balloon 24576 \
2026-05-29 08:00:52 +02:00
--cores 6 \
--cpu host \
--machine q35 \
--bios ovmf \
--efidisk0 local:1,format=raw \
--net0 virtio,bridge=vmbr0 \
--ostype other \
--scsihw virtio-scsi-single \
--scsi0 local:80,format=raw \
2026-05-29 08:25:02 +02:00
--cdrom local:iso/FreeBSD-15.0-RELEASE-amd64-disc1.iso \
2026-05-29 08:00:52 +02:00
--boot order=ide2;scsi0 \
--agent enabled=1
```
Via UI:
```
2026-05-29 08:45:19 +02:00
General → Name: zamolxis
2026-05-29 08:25:02 +02:00
OS → FreeBSD (other), ISO: FreeBSD-15.0-RELEASE-amd64-disc1.iso
2026-05-29 08:00:52 +02:00
System → Machine: q35, BIOS: OVMF (UEFI), Qemu Agent: ✓
Disks → VirtIO SCSI, 80GB, Cache: None
CPU → 6 cores, Type: host
2026-05-29 08:45:19 +02:00
Memory → 24576 MB
2026-05-29 08:00:52 +02:00
Network → VirtIO, vmbr0
```
2026-05-29 08:45:19 +02:00
### 3. Create decebal / ID 901 (run on the SSD host — sole VM)
2026-05-29 08:00:52 +02:00
```sh
# VM with OS disk
2026-05-29 08:45:19 +02:00
qm create 901 \
--name decebal \
--memory 30720 \
2026-05-29 08:00:52 +02:00
--balloon 0 \
2026-05-29 08:45:19 +02:00
--cores 6 \
2026-05-29 08:00:52 +02:00
--cpu host \
--machine q35 \
--bios ovmf \
--efidisk0 local:1,format=raw \
--net0 virtio,bridge=vmbr0 \
--ostype other \
--scsihw virtio-scsi-single \
2026-05-29 08:13:55 +02:00
--scsi0 local:20,format=raw,discard=on,ssd=1 \
2026-05-29 08:25:02 +02:00
--cdrom local:iso/FreeBSD-15.0-RELEASE-amd64-disc1.iso \
2026-05-29 08:00:52 +02:00
--boot order=ide2;scsi0 \
--agent enabled=1
# Add second disk for ZFS data pool
2026-05-29 08:45:19 +02:00
qm set 901 --scsi1 local:100,format=raw,discard=on,ssd=1
2026-05-29 08:00:52 +02:00
```
`--balloon 0` disables memory ballooning on the DB VM. This is required — ballooning can silently reclaim pages from the InnoDB buffer pool under host memory pressure.
### 4. FreeBSD installer settings (same for both VMs)
```
Welcome screen → Install
Keymap → your preference
2026-05-29 08:45:19 +02:00
Hostname → zamolxis (or decebal)
2026-05-29 08:00:52 +02:00
Distribution → base, kernel (nothing else)
Partitioning → Auto (ZFS)
Pool type → stripe (single disk)
Disk → da0 (the OS disk)
Swap → 2GB
Compress → lz4
Encrypt → No
Network → vtnet0, configure with a temporary IP for initial pkg bootstrap
(Tailscale will take over; this IP can be removed afterwards)
Mirror → closest region
Root password → set strong password
SSHD → enable
```
### 5. First boot — both VMs
```sh
# Update base system
freebsd-update fetch install
# Bootstrap pkg and install essentials
pkg update && pkg upgrade -y
pkg install -y qemu-guest-agent tailscale sudo curl wget
2026-05-29 08:25:02 +02:00
# Verify package versions available
pkg search php85 | head -3 # should show 8.5.6+
pkg search mysql84-server # should show 8.4.9
pkg search redis | grep "^redis" # should show 8.6.x
2026-05-29 08:00:52 +02:00
# Enable QEMU guest agent (so Proxmox can see VM IP, issue graceful shutdowns)
sysrc qemu_guest_agent_enable=YES
service qemu-guest-agent start
# Join Tailscale network
sysrc tailscaled_enable=YES
service tailscaled start
2026-05-29 08:45:19 +02:00
tailscale up --hostname=zamolxis # or decebal
2026-05-29 08:00:52 +02:00
# Note the assigned Tailscale IP — plug into pf.conf and wp-config placeholders
tailscale ip -4
```
2026-05-29 08:45:19 +02:00
After both VMs have Tailscale IPs, replace all `<zamolxis Tailscale IP>` and `<decebal Tailscale IP>` placeholders throughout this document and in all config files.
2026-05-29 08:00:52 +02:00
---
2026-05-29 07:45:10 +02:00
## 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
```
---
2026-05-29 08:45:19 +02:00
## Phase 1 — DB Node (decebal)
2026-05-29 07:45:10 +02:00
### 1.1 FreeBSD base + ZFS
```sh
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>
2026-05-29 10:19:38 +02:00
# During FreeBSD 15.0 install: enable ZFS, create OS pool on first disk (da0)
# da1 is left raw for the database ZFS pool
2026-05-29 07:45:10 +02:00
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
2026-05-29 08:25:02 +02:00
MySQL 8.4.9 is the correct choice. MySQL 9.0 was removed from FreeBSD ports (EOL). MySQL 9.1 expired March 2026 (EOL). The 9.x series is Oracle's "innovation" track with no long-term support guarantee. MySQL 8.4 is the current LTS branch.
2026-05-29 07:45:10 +02:00
```sh
pkg install mysql84-server
sysrc mysql_enable=YES
sysrc mysql_dbdir=/var/db/mysql
```
`/usr/local/etc/mysql/my.cnf` :
```ini
[mysqld]
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>
2026-05-29 10:19:38 +02:00
# Network
bind-address = 100.67.166.29
port = 3306
mysqlx = 0
# Paths
2026-05-29 07:45:10 +02:00
datadir = /var/db/mysql
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>
2026-05-29 10:19:38 +02:00
socket = /var/db/mysql/mysql.sock
log_error = /var/db/mysql/decebal.err
pid-file = /var/db/mysql/decebal.pid
# InnoDB
2026-05-29 08:45:19 +02:00
innodb_buffer_pool_size = 26G
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>
2026-05-29 10:19:38 +02:00
innodb_buffer_pool_instances = 8
innodb_redo_log_capacity = 2G
2026-05-29 07:45:10 +02:00
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
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>
2026-05-29 10:19:38 +02:00
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# Connections
2026-05-29 07:45:10 +02:00
max_connections = 150
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>
2026-05-29 10:19:38 +02:00
thread_cache_size = 16
table_open_cache = 4000
# Character set
2026-05-29 07:45:10 +02:00
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
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>
2026-05-29 10:19:38 +02:00
# Logging
slow_query_log = 1
slow_query_log_file = /var/db/mysql/slow.log
long_query_time = 2
binlog_expire_logs_seconds = 259200
# Safety
2026-05-29 07:45:10 +02:00
skip-name-resolve
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>
2026-05-29 10:19:38 +02:00
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
2026-05-29 07:45:10 +02:00
```
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>
2026-05-29 10:19:38 +02:00
Note: `innodb_redo_log_capacity` replaces the deprecated `innodb_log_file_size` in MySQL 8.4.
2026-05-29 07:45:10 +02:00
### 1.3 pf firewall
`/etc/pf.conf` :
```
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>
2026-05-29 10:19:38 +02:00
# decebal — database server pf ruleset
2026-05-29 07:45:10 +02:00
ext_if = "vtnet0"
ts_if = "tailscale0"
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>
2026-05-29 10:19:38 +02:00
# 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"
2026-05-29 07:45:10 +02:00
set skip on lo0
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>
2026-05-29 10:19:38 +02:00
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
2026-05-29 07:45:10 +02:00
```
```sh
sysrc pf_enable=YES
service pf start
```
### 1.4 Tailscale
```sh
pkg install tailscale
sysrc tailscaled_enable=YES
service tailscaled start
2026-05-29 08:45:19 +02:00
tailscale up --hostname=decebal
2026-05-29 07:45:10 +02:00
```
### 1.5 DB restore
```sh
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>
2026-05-29 10:19:38 +02:00
# Dump from old transilvan (port 79), import to new decebal
# Run from a machine with access to both nodes
ssh -p 79 root@transilvan \
2026-05-29 07:45:10 +02:00
"mysqldump -S /tmp/mysqld.sock --single-transaction \
--routines --triggers --events sql_sibiuindepen" \
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>
2026-05-29 10:19:38 +02:00
| ssh root@100 .67.166.29 \
"mysql --socket=/var/db/mysql/mysql.sock sql_sibiuindepen"
2026-05-29 07:45:10 +02:00
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>
2026-05-29 10:19:38 +02:00
# App user already created — zamolxis Tailscale IP 100.115.128.41
# Credentials: sql_sibiuindepen / see keepass
2026-05-29 07:45:10 +02:00
```
### 1.6 DB backup cron
`/root/db-backup.sh` :
```sh
#!/bin/sh
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>
2026-05-29 10:19:38 +02:00
# 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"
2026-05-29 07:45:10 +02:00
```
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>
2026-05-29 10:19:38 +02:00
Crontab (root, on decebal):
2026-05-29 07:45:10 +02:00
```
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>
2026-05-29 10:19:38 +02:00
# 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
2026-05-29 07:45:10 +02:00
```
---
2026-05-29 08:45:19 +02:00
## Phase 2 — Web Node (zamolxis)
2026-05-29 07:45:10 +02:00
### 2.1 Base packages
2026-05-29 08:25:02 +02:00
Redis 8.x (currently 8.6.3 in ports) is the current major version.
2026-05-29 07:45:10 +02:00
```sh
2026-05-29 08:25:02 +02:00
pkg install -y nginx redis git curl wget
2026-05-29 07:45:10 +02:00
sysrc nginx_enable=YES redis_enable=YES
service redis start
```
2026-05-29 08:25:02 +02:00
### 2.2 PHP 8.5 from binary packages
2026-05-29 07:45:10 +02:00
2026-05-29 08:25:02 +02:00
On FreeBSD 15.0, PHP 8.5 and all extensions are available as binary packages — no ports compilation required.
2026-05-29 07:45:10 +02:00
2026-05-29 08:25:02 +02:00
```sh
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>
2026-05-29 10:19:38 +02:00
# Base PHP 8.5 + FPM (opcache is bundled in the base php85 package)
pkg install -y php85
2026-05-29 08:25:02 +02:00
# Extensions needed for WordPress
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>
2026-05-29 10:19:38 +02:00
# Note: imagick and redis are PECL extensions — package prefix is php85-pecl-*
2026-05-29 08:25:02 +02:00
pkg install -y \
php85-bcmath \
php85-curl \
php85-exif \
php85-fileinfo \
php85-gd \
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>
2026-05-29 10:19:38 +02:00
php85-pecl-imagick \
2026-05-29 08:25:02 +02:00
php85-intl \
php85-mbstring \
php85-mysqli \
php85-pcntl \
php85-pdo_mysql \
php85-posix \
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>
2026-05-29 10:19:38 +02:00
php85-pecl-redis \
2026-05-29 08:25:02 +02:00
php85-soap \
php85-sockets \
php85-sodium \
php85-xml \
php85-xmlreader \
php85-xmlwriter \
php85-xsl \
php85-zip
pkg install -y ImageMagick7
2026-05-29 07:45:10 +02:00
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; }
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>
2026-05-29 10:19:38 +02:00
if ($query_string) { set $skip_cache 1; } # nginx if does not support !=
2026-05-29 07:45:10 +02:00
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` :
```
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>
2026-05-29 10:19:38 +02:00
# zamolxis — web server pf ruleset
2026-05-29 07:45:10 +02:00
ext_if = "vtnet0"
ts_if = "tailscale0"
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>
2026-05-29 10:19:38 +02:00
# 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 }"
2026-05-29 07:45:10 +02:00
set skip on lo0
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>
2026-05-29 10:19:38 +02:00
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
2026-05-29 07:45:10 +02:00
```
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>
2026-05-29 10:19:38 +02:00
Note: tighten the HTTP rule to NPM's specific Tailscale IP once confirmed.
2026-05-29 07:45:10 +02:00
```sh
sysrc pf_enable=YES
service pf start
```
### 2.6 Tailscale
```sh
pkg install tailscale
sysrc tailscaled_enable=YES
service tailscaled start
2026-05-29 08:45:19 +02:00
tailscale up --hostname=zamolxis
2026-05-29 07:45:10 +02:00
```
### 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
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>
2026-05-29 10:19:38 +02:00
// 1. Update DB_HOST to decebal Tailscale IP
define( 'DB_HOST', '100.67.166.29' );
2026-05-29 07:45:10 +02:00
// 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
2026-05-29 08:45:19 +02:00
### Pre-cutover checklist (run on zamolxis before touching NPM)
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
Steps 1– 8 below are already verified. Steps 9– 10 require the DB import (done at cutover).
2026-05-29 07:45:10 +02:00
```sh
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
# 1. PHP extensions — VERIFIED OK
php -m | grep -E "imagick|redis|pdo_mysql|intl|soap|sodium|gd|mbstring"
# opcache: verified active via FPM web request
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
# 2. Services running — VERIFIED OK
service nginx status && service php_fpm status && service redis status
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
# 3. Redis socket responding — VERIFIED OK
redis-cli -s /var/run/redis/redis.sock ping # → PONG
# 4. PHP-FPM socket present — VERIFIED OK
ls /var/run/php-fpm-si.sock
# 5. DB connectivity zamolxis->decebal — VERIFIED OK
# Note: use credentials file to avoid ! shell expansion issue
cat > /tmp/.chk.cnf << EOF
[client]
user=sql_sibiuindepen
password=W0rdPr3ss@SI2026 !
host=100.67.166.29
port=3306
EOF
mysql --defaults-file=/tmp/.chk.cnf --get-server-public-key \
-e "SELECT 1 AS db_ok;"
rm -f /tmp/.chk.cnf
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
# 6. WordPress core files present — VERIFIED OK
ls /var/www/sibiuindependent.ro/index.php /var/www/sibiuindependent.ro/wp-login.php
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
# 7. Redis Object Cache drop-in installed, W3TC absent — VERIFIED OK
ls /var/www/sibiuindependent.ro/wp-content/object-cache.php
ls /var/www/sibiuindependent.ro/wp-content/plugins/w3-total-cache 2>/dev/null || echo "W3TC absent (good)"
# 8. nginx responds (MISS expected without DB) — VERIFIED OK
curl -sI http://127.0.0.1/ -H "Host: sibiuindependent.ro" | grep X-Cache-Status
# --- Steps 9-10: run AFTER DB import at cutover ---
# 9. WordPress returns 200 (post DB import)
curl -sk -o /dev/null -w "%{http_code}" \
http://127.0.0.1/ -H "Host: sibiuindependent.ro"
# Expected: 200
# 10. Cache HIT on second request (post DB import)
curl -sI http://127.0.0.1/ -H "Host: sibiuindependent.ro" | grep X-Cache-Status # MISS
curl -sI http://127.0.0.1/ -H "Host: sibiuindependent.ro" | grep X-Cache-Status # HIT
# 11. Redis object cache connected (post DB import)
wp --path=/var/www/sibiuindependent.ro --allow-root redis enable
wp --path=/var/www/sibiuindependent.ro --allow-root redis status
2026-05-29 07:45:10 +02:00
```
### Cutover sequence
```
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
T-0 Disable cache warmer crons on OLD dracula (comment out both lines in crontab)
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
T-1 Final rsync — incremental delta only, excludes W3TC and cache:
# Run on zamolxis:
rsync -avz \
-e "ssh -i /root/.ssh/migration_key -p 79 -o StrictHostKeyChecking=no" \
2026-05-29 07:45:10 +02:00
--exclude="wp-content/cache/" \
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
--exclude="wp-content/w3tc-config/" \
--exclude="wp-content/plugins/w3-total-cache/" \
--exclude="wp-content/uploads/wpo_wcml_cache/" \
root@100 .99.157.56:/www/wwwroot/sibiuindependent.ro/ \
/var/www/sibiuindependent.ro/
# Then immediately run fixup (rewrites wp-config, re-installs Redis drop-in):
/root/post-rsync-fixup.sh
2026-05-29 07:45:10 +02:00
T-2 Final DB dump and import:
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
# Run from any host with access to both (or from this local machine):
ssh -p 79 root@100 .99.157.56 \
2026-05-29 07:45:10 +02:00
"mysqldump -S /tmp/mysqld.sock --single-transaction \
--routines --triggers --events sql_sibiuindepen" \
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
| ssh root@100 .67.166.29 \
"mysql --socket=/var/db/mysql/mysql.sock sql_sibiuindepen"
# ZFS snapshot immediately after import:
ssh root@100 .67.166.29 \
"zfs snapshot data/mysql@post -migration-$(date +%Y%m%d)"
T-2.5 Run WP-CLI to enable Redis and activate Redis Object Cache plugin:
# Run on zamolxis after DB is imported:
wp --path=/var/www/sibiuindependent.ro --allow-root redis enable
wp --path=/var/www/sibiuindependent.ro --allow-root plugin activate redis-cache
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
T-3 Checklist steps 9– 11 (see above): verify 200, cache HIT, Redis status
T-4 Switch NPM upstream:
2026-05-29 07:45:10 +02:00
old: 100.99.157.56 (dracula Tailscale IP)
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>
2026-05-29 10:19:38 +02:00
new: 100.115.128.41 (zamolxis Tailscale IP)
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
(single upstream change in NPM UI — instant, no DNS TTL)
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
T-5 Smoke test:
- Homepage loads (public, non-logged-in)
2026-05-29 07:45:10 +02:00
- An article page loads
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
- wp-admin accessible and working
- Second page load for any article: X-Cache-Status: HIT in response headers
2026-05-29 07:45:10 +02:00
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
T-6 Enable cache warmer cron on zamolxis (already installed, just runs at 03:00/15:00)
# Optionally run immediately:
/root/cache-warmer.sh &
2026-05-29 07:45:10 +02:00
```
**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)
```
---
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>
2026-05-29 10:19:38 +02:00
## Open Items
- [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
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
- [x] PHP 8.5 — all extensions installed; PECL packages use `php85-pecl-*` prefix; opcache bundled in base php85; phar installed for WP-CLI
- [x] Redis Object Cache plugin — downloaded and drop-in installed at wp-content/object-cache.php
- [ ] W3TC minify in use? — confirm before cutover; if yes: drop (nginx serves pre-minified assets) or use Autoptimize as replacement
- [x] ZFS pool — data pool on /dev/da1 (100GB) ONLINE, data/mysql at /var/db/mysql
- [x] MySQL 8.4.9 LTS — deployed on decebal
- [x] Beszel and Telegraf — dropped
- [x] Redis — local to zamolxis, unix socket
- [x] Phase 2 — rsync complete (7.1GB, exit 0), wp-config updated, W3TC removed
- [x] Phase 2 — pre-cutover checklist steps 1– 8 all passed
- [ ] Phase 3 — DB import at cutover (checklist steps 9– 11 post-import)
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>
2026-05-29 10:19:38 +02:00
- [ ] Phase 3 — NPM upstream switch 100.99.157.56 → 100.115.128.41
## Deployment Status (as of 2026-05-29)
| Node | Status | Notes |
|---|---|---|
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
| zamolxis | **Ready for cutover ** | All services running, 6.8GB webroot synced, checklist items 1– 8 passed |
| decebal | **Ready for cutover ** | MySQL 8.4.9 running, ZFS ONLINE, app user verified, backup cron installed |
### What was tuned for 1000 concurrent users (5× baseline)
| Setting | zamolxis | decebal |
|---|---|---|
| `kern.maxfiles` | 200,000 | 200,000 |
| `kern.ipc.somaxconn` | 4096 | 1024 |
| `net.inet.tcp.sendspace` | 262,144 | 131,072 |
| `vfs.zfs.arc_max` | — | 2GB (leaves RAM for InnoDB) |
| PHP-FPM `pm.max_children` | 80 (was 40) | — |
| nginx `worker_connections` | 8192 (was 4096) | — |
| nginx `worker_rlimit_nofile` | 65536 | — |
| MySQL `max_connections` | — | 300 (was 150) |
| WP-CLI | Installed at /usr/local/bin/wp | — |
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>
2026-05-29 10:19:38 +02:00
### Connectivity verified
- zamolxis → decebal MySQL (100.67.166.29:3306) as `sql_sibiuindepen` : OK
feat: phase 2 complete — zamolxis ready for cutover
- rsync: 7.1GB transferred from dracula (exit 0), all years 2012-2026
- wp-config.php: DB_HOST=100.67.166.29, new DB password, Redis constants
- W3TC: removed (plugin + all drop-ins: db.php, advanced-cache.php)
- W3TC db.php drop-in was W3TC module, not index-wp-mysql-for-speed
- Redis Object Cache: plugin installed, object-cache.php drop-in active
- .user.ini: open_basedir updated from AApanel path to /var/www/...
- WP-CLI: installed (required php85-phar, php85-ctype, php85-filter,
php85-tokenizer which were not in the base php85 package)
- Kernel tuning: sysctl.conf on both nodes for 1000+ concurrent users
- zamolxis: kern.maxfiles=200k, somaxconn=4096, tcp buffers 256KB
- decebal: vfs.zfs.arc_max=2GB (critical — prevents ZFS evicting InnoDB)
- PHP-FPM: pm.max_children 40→80, start_servers 8→16, spare 16→32
- nginx: worker_connections 4096→8192, worker_rlimit_nofile=65536
- MySQL: max_connections 150→300 (live SET GLOBAL + my.cnf updated)
- Pre-cutover checklist items 1-8: all passed
- Cutover sequence updated with correct rsync excludes and post-rsync-fixup.sh
Pending at cutover: DB import, checklist steps 9-11, NPM upstream switch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:10:28 +02:00
- pf on decebal: only 100.115.128.41 reaches port 3306: OK
- Backup cron: /var/db/backups/mysql/sql_sibiuindepen_*.sql.gz runs daily at 02:00
- nginx fastcgi_cache + PHP-FPM + Redis all responding on zamolxis
### Known issue to fix at cutover
- W3TC plugin will be re-added by the final rsync (it's on the source). The cutover rsync command above uses `--exclude="wp-content/plugins/w3-total-cache/"` to prevent this. `/root/post-rsync-fixup.sh` must be run after every rsync.