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` |
| 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
- **Monitoring:** Telegraf official package. Tailscale official package. Beszel: Go binary works
- **Wazuh:** Community source build only — dropped per requirements
- **Verdict:** Strongest all-round candidate. PHP 8.5 + all extensions + MySQL 8.4 + ZFS + pf + official Tailscale
### Gentoo
- **Minimalism:** Genuine — USE flags compile exactly what is needed, OpenRC available
- **PHP 8.5:** In portage (rolling release guarantees latest)
- **MySQL 8.4:** In portage
- **All agents:** Still Linux, everything works natively
- **Maintenance:** High ongoing cost — every update is a compile run. Production liability
- **Verdict:** Philosophically compelling, operationally expensive for a production news site
### Alpine Linux
- **Minimalism:** musl libc + busybox, ~130MB base
- **PHP 8.5:** Community repos, most extensions packaged
- **Init:** OpenRC, no systemd
- **Migration effort:** Lowest — still Linux, apk replaces apt, paths similar
- **Verdict:** Best minimal Linux option; easier than FreeBSD but fewer operational benefits (no ZFS, no jails)
### Decision: **FreeBSD 14.x on both nodes**
Tiebreaker: ZFS native on the DB node. `zfs snapshot` before any migration step, before any MySQL upgrade, before any schema change — this alone justifies the choice. PHP 8.5 builds from ports cleanly. MySQL 8.4 LTS is in pkg. All required PHP extensions are in ports. pf is simpler and more powerful than nftables for these firewall rules. Tailscale has official FreeBSD support.
Both hosts run Proxmox VE (confirmed via qemu-guest-agent on current VMs). New VMs are the sole tenants on their respective hosts.
---
## 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 |
| 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 |
| InnoDB buffer pool hit rate | >99% | 20GB pool, 9.6GB dataset fits entirely in RAM |
| Active DB connections | ≤ 40 | One per active PHP-FPM worker |
### dracula-new (web VM on HDD host)
| Parameter | Value |
|---|---|
| vCPU | 6 |
| RAM | 14GB |
| Disk | 80GB single virtual disk (HDD-backed) |
| Balloon | Enabled (web VM, acceptable) |
RAM breakdown: PHP-FPM peak 1.4GB + Redis 512MB + nginx + UBC page cache for hot articles (~4–6GB effective) + OS 1GB + headroom.
HDD is acceptable for the web node. The fastcgi_cache working set for a news site fits in UBC (RAM). HDD latency only affects cold cache startup and log writes (sequential). The CPU and PHP-FPM worker slots are the actual bottleneck, not disk.
**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.
`--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
Hostname → dracula-new (or transilvan-new)
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)
# 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
tailscale up --hostname=dracula-new # or transilvan-new
# Note the assigned Tailscale IP — plug into pf.conf and wp-config placeholders
tailscale ip -4
```
After both VMs have Tailscale IPs, replace all `<dracula-new Tailscale IP>` and `<transilvan-new Tailscale IP>` placeholders throughout this document and in all config files.
- [ ] Confirm new Tailscale hostnames for both nodes (determines pf rules and wp-config DB_HOST)
- [ ] Confirm NPM node Tailscale IP (determines pf rule on dracula-new)
- [ ] PHP 8.5 from ports — attempt first, fall back to pkg php84 if dependency issue
- [ ] Redis Object Cache plugin — already in plugins dir or fresh install needed
- [ ] W3TC features in use beyond caching: minify? If yes, switch to standalone nginx-based minification or keep W3TC minify only with nginx page cache
- [ ] ZFS pool disk — confirm second disk available on transilvan-new VM
- [ ] MySQL 8.4 vs MariaDB 11.4 LTS — plan targets MySQL 8.4; revisit if preferred
- [x] Beszel and Telegraf — dropped. Proxmox native VM monitoring is sufficient.
- [x] Redis placement — local to dracula-new (unix socket). No 3rd host. WordPress object cache uses 50–150MB RAM; a dedicated VM would cost more resources than Redis itself consumes. A separate Redis host is only justified when scaling to multiple web nodes sharing a single cache.