- Renamed README.md to sibiuindependent.ro.MD - Status updated to LIVE as of 2026-05-31 - All open items resolved and documented - Added post-cutover issues & fixes section (8 issues): SimpleXML/WPS3Media, Perfmatters login URL, InformatiQ-Toolkit 301→example.com, empty permalink_structure, Redis plugin inactive, backup cron wrong format, Redis maxmemory unbounded, nginx missing webp - Updated pf rules (zamolxis HTTP locked to NPM 100.89.238.4) - Corrected decebal crontab format (no user field in user crontab) - Current performance metrics from 2026-05-31 health check - Deployment status table with all items checked Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
23 KiB
sibiuindependent.ro — Stack Migration
May 2026
Migration of sibiuindependent.ro from Debian 12 + AApanel to FreeBSD 15.0 minimal stack.
Status: LIVE as of 2026-05-31. Migration complete.
Live Architecture
[internet] → [NPM node, 100.89.238.4 Tailscale] → [zamolxis 100.115.128.41] → [decebal 100.67.166.29]
| Node | Role | OS | vCPU | RAM | Disk | Tailscale IP |
|---|---|---|---|---|---|---|
| zamolxis | Web | FreeBSD 15.0-p9 | 6 | 24GB | 80GB HDD | 100.115.128.41 |
| decebal | Database | FreeBSD 15.0-p9 | 6 | 30GB | 20GB OS + 100GB ZFS data (SSD) | 100.67.166.29 |
Both VMs are on separate bare-metal Proxmox hosts. All inter-service traffic runs over Tailscale. NPM handles TLS termination. No public-facing ports on zamolxis or decebal.
Stack
zamolxis (web)
| Component | Version / Detail |
|---|---|
| OS | FreeBSD 15.0-RELEASE-p9 |
| Web server | nginx 1.30.2 |
| PHP | 8.5.4 + FPM (unix socket /var/run/php-fpm-si.sock) |
| Page cache | nginx fastcgi_cache (/var/cache/nginx/si, 4GB max) |
| Object cache | Redis Object Cache plugin v2.8.0 (Till Krüss) via unix socket |
| Bytecode cache | OPcache 256MB, 20000 files, JIT disabled (caused FPM crashes with WordPress plugin stacks) |
| Redis | 8.6.2, unix socket /var/run/redis/redis.sock, maxmemory 3GB allkeys-lru |
| Sessions | Redis via session.save_path = unix:///var/run/redis/redis.sock |
| Media | wp-content/uploads local (6.5GB), S3 offload via WPS3Media to s3.palmasolutions.net/sibiuindependent (101,145 files in ddf44d_as3cf_items) |
| Login URL | /intrarepresa (Perfmatters custom login — /wp-login.php and /wp-admin return 403) |
| WP prefix | ddf44d_ |
| Firewall | pf — HTTP only from NPM (100.89.238.4), SSH only from mgmt + Tailscale |
Active PHP extensions: bcmath, curl, exif, fileinfo, gd, imagick, intl, mbstring, mysqli, opcache, pcntl, pdo_mysql, pecl-redis, posix, simplexml, soap, sockets, sodium, xml, xmlreader, xmlwriter, xsl, zip
Note: php85-simplexml is required by WPS3Media (AWS SDK XML parsing). Without it, the entire WPS3Media plugin fails to instantiate silently.
decebal (db)
| Component | Version / Detail |
|---|---|
| OS | FreeBSD 15.0-RELEASE-p9 |
| Database | MySQL 8.4.9 LTS |
| Database name | sql_sibiuindepen |
| Socket | /var/db/mysql/mysql.sock |
| Data directory | /var/db/mysql (ZFS pool data/mysql) |
| ZFS settings | recordsize=16K, compression=lz4, primarycache=metadata |
| InnoDB buffer pool | 26GB (dataset ~1.85GB — fits entirely in RAM) |
| Firewall | pf — MySQL only from zamolxis (100.115.128.41), SSH only from mgmt + Tailscale |
| Backup | /root/db-backup.sh daily 02:00, 7-day retention in /var/db/backups/mysql/ |
| ZFS snapshots | Weekly (Sunday 00:30), last 4 kept |
Migration Source (decommissioned)
| Node | Role | OS | Tailscale IP |
|---|---|---|---|
| dracula | Web | Debian 12 + AApanel | 100.99.157.56 |
| transilvan | Database | Debian 12 + AApanel | — |
| transilvan MySQL socket | — | — | /tmp/mysqld.sock |
OS Selection
Candidates: OpenBSD, FreeBSD, Gentoo, Alpine Linux
OpenBSD — best security (pledge, unveil, W^X), but PHP 8.5 not in packages, MySQL 8.x absent, imagick friction. Viable only with PHP 8.4 + MariaDB.
FreeBSD — pf, Capsicum, ZFS native, PHP 8.5 + all extensions as binary packages, MySQL 8.4 LTS, Redis 8.x, official Tailscale. Strongest all-round candidate.
Gentoo — genuine minimalism via USE flags, but every update is a compile run. Production liability.
Alpine Linux — easiest migration (still Linux), musl + busybox, apk. No ZFS, no jails. Best minimal Linux option.
Decision: FreeBSD 15.0 — ZFS native on the DB node is the tiebreaker (zfs snapshot before any migration step). FreeBSD 15.0 over 14.x specifically because PHP 8.5 is a binary package on 15.0.
What Was Dropped
| Component | Reason |
|---|---|
| AApanel / btpanel | Replaced by direct config management |
| W3 Total Cache | Replaced by nginx fastcgi_cache + Redis Object Cache |
| 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 |
| Wazuh / Five Nines / Beszel / Telegraf | Not required — Proxmox native monitoring covers VM metrics |
| memory-cleanup.sh | Linux /proc hack, was on wrong server anyway |
| ngxblocker | Not applicable |
| AApanel cron scripts | Panel-specific: acme renewal, site_total, backup.py |
Caching Architecture
| Layer | Old | New |
|---|---|---|
| Page cache | W3TC on-disk (4.8GB, PHP-generated) | nginx fastcgi_cache (PHP bypassed entirely on HIT) |
| Object cache | W3TC + Memcached (TCP) | Redis Object Cache + Redis (unix socket) |
| Bytecode cache | OPcache | OPcache (JIT disabled) |
| Sessions | PHP file sessions | Redis via unix socket |
Host Hardware
| Host | CPU | RAM | Storage | VM |
|---|---|---|---|---|
| zamolxis host | Xeon E5-1620 v2 @ 3.70GHz (8 logical) | 64GB | Datacenter HDD | zamolxis (ID 900) |
| decebal host | Xeon E5-1620 v2 @ 3.70GHz (8 logical) | 32GB | SSD 160GB | decebal (ID 901) — sole VM |
VM Provisioning
1. FreeBSD 15.0 ISO
wget -P /var/lib/vz/template/iso/ \
https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/15.0/FreeBSD-15.0-RELEASE-amd64-disc1.iso
2. zamolxis (ID 900, HDD host)
qm create 900 \
--name zamolxis \
--memory 24576 --balloon 24576 \
--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 \
--cdrom local:iso/FreeBSD-15.0-RELEASE-amd64-disc1.iso \
--boot order=ide2;scsi0 \
--agent enabled=1
3. decebal (ID 901, SSD host — sole VM)
qm create 901 \
--name decebal \
--memory 30720 --balloon 0 \
--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:20,format=raw,discard=on,ssd=1 \
--cdrom local:iso/FreeBSD-15.0-RELEASE-amd64-disc1.iso \
--boot order=ide2;scsi0 \
--agent enabled=1
qm set 901 --scsi1 local:100,format=raw,discard=on,ssd=1
--balloon 0 is required — memory ballooning must not reclaim InnoDB buffer pool pages.
4. FreeBSD installer settings (both VMs)
Welcome → Install
Hostname → zamolxis (or decebal)
Distribution → base, kernel only
Partitioning → Auto (ZFS), stripe, da0, swap 2GB, lz4
Network → vtnet0, temporary IP for pkg bootstrap
SSHD → enable
5. First boot (both VMs)
freebsd-update fetch install
pkg update && pkg upgrade -y
pkg install -y qemu-guest-agent tailscale sudo curl wget
sysrc qemu_guest_agent_enable=YES
service qemu-guest-agent start
sysrc tailscaled_enable=YES
service tailscaled start
tailscale up --hostname=zamolxis # or decebal
tailscale ip -4
Phase 1 — DB Node (decebal)
ZFS
zpool create -o ashift=12 data /dev/da1
zfs create -o mountpoint=/var/db/mysql data/mysql
zfs set recordsize=16K data/mysql
zfs set primarycache=metadata data/mysql
zfs set compression=lz4 data/mysql
MySQL 8.4 LTS
MySQL 8.4.9 is the current LTS branch. MySQL 9.0/9.1 were Oracle's "innovation" track — both EOL and removed from FreeBSD ports.
pkg install mysql84-server
sysrc mysql_enable=YES
sysrc mysql_dbdir=/var/db/mysql
/usr/local/etc/mysql/my.cnf:
[mysqld]
bind-address = 100.67.166.29
port = 3306
mysqlx = 0
datadir = /var/db/mysql
socket = /var/db/mysql/mysql.sock
log_error = /var/db/mysql/decebal.err
pid-file = /var/db/mysql/decebal.pid
innodb_buffer_pool_size = 26G
innodb_buffer_pool_instances = 8
innodb_redo_log_capacity = 2G
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
max_connections = 300
thread_cache_size = 16
table_open_cache = 4000
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
slow_query_log = 1
slow_query_log_file = /var/db/mysql/slow.log
long_query_time = 2
binlog_expire_logs_seconds = 259200
skip-name-resolve
sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
[client]
socket = /var/db/mysql/mysql.sock
Note: innodb_redo_log_capacity replaces deprecated innodb_log_file_size in MySQL 8.4.
pf (decebal)
/etc/pf.conf:
ext_if = "vtnet0"
ts_if = "tailscale0"
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_ts = "100.115.128.41"
set skip on lo0
set block-policy drop
scrub in all
block in all
block out quick inet6 all
pass out all keep state
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
pass in on $ts_if proto tcp from $zamolxis_ts to any port 3306 keep state
DB Backup (decebal)
/root/db-backup.sh:
#!/bin/sh
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"
Root crontab on decebal (crontab -l):
# Daily MySQL dump at 02:00
0 2 * * * /root/db-backup.sh
# Weekly ZFS snapshot (Sunday 00:30)
30 0 * * 0 zfs snapshot data/mysql@weekly-$(date +%Y%m%d)
# Prune ZFS snapshots older than 4 weeks (Sunday 01:00)
0 1 * * 0 zfs list -H -t snapshot -o name data/mysql | sort | head -n -4 | xargs -r zfs destroy
Note: user crontab format — no user field between time spec and command (that is /etc/crontab format).
Phase 2 — Web Node (zamolxis)
Packages
pkg install -y nginx redis git curl wget
sysrc nginx_enable=YES redis_enable=YES
service redis start
pkg install -y php85 \
php85-bcmath php85-curl php85-exif php85-fileinfo \
php85-gd php85-pecl-imagick php85-intl php85-mbstring \
php85-mysqli php85-pcntl php85-pdo_mysql php85-posix \
php85-pecl-redis php85-simplexml php85-soap php85-sockets \
php85-sodium php85-xml php85-xmlreader php85-xmlwriter \
php85-xsl php85-zip ImageMagick7
sysrc php_fpm_enable=YES
php85-simplexml is mandatory — WPS3Media uses the AWS SDK which requires SimpleXML. Without it the plugin silently fails to instantiate.
PHP-FPM pool
/usr/local/etc/php-fpm.d/sibiuindependent.conf:
[sibiuindependent]
user = www
group = www
listen = /var/run/php-fpm-si.sock
listen.owner = www
listen.group = www
pm = dynamic
pm.max_children = 80
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):
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.jit = 0
opcache.jit_buffer_size = 0
session.save_handler = redis
session.save_path = "unix:///var/run/redis/redis.sock"
JIT is explicitly disabled. WordPress plugin stacks (especially Yoast + TagDiv composer) caused PHP-FPM crashes under JIT on this install.
nginx
/usr/local/etc/nginx/nginx.conf:
worker_processes auto;
worker_rlimit_nofile 65536;
events { worker_connections 8192; }
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; }
if ($query_string) { set $skip_cache 1; }
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|webp|svg|ico|woff|woff2|ttf|eot|mp4|webm|ogv)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
}
}
}
webp must be in the static location regex. WPS3Media serves WebP variants from S3; without it, .webp requests fall through to PHP-FPM and trigger a full WordPress load for a file that doesn't exist locally.
Redis
/usr/local/etc/redis.conf (relevant):
maxmemory 3gb
maxmemory-policy allkeys-lru
unixsocket /var/run/redis/redis.sock
unixsocketperm 777
pf (zamolxis)
/etc/pf.conf:
ext_if = "vtnet0"
ts_if = "tailscale0"
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 }"
npm_ts = "100.89.238.4"
set skip on lo0
set block-policy drop
scrub in all
block in all
block out quick inet6 all
pass out all keep state
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
pass in on $ts_if proto tcp from $npm_ts to any port 80 keep state
wp-config.php changes
define( 'DB_HOST', '100.67.166.29' );
define( 'DB_PASSWORD', 'W0rdPr3ss@SI2026!' );
define( 'WP_REDIS_SCHEME', 'unix' );
define( 'WP_REDIS_PATH', '/var/run/redis/redis.sock' );
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );
post-rsync-fixup.sh
/tmp/post-rsync-fixup.sh — run after every rsync:
- Rewrites
DB_HOSTandDB_PASSWORDin wp-config.php - Removes W3TC plugin and its drop-ins (advanced-cache.php, db.php)
- Installs Redis Object Cache drop-in if missing
- Fixes
.user.iniopen_basedir - Fixes ownership (
www:www) and wp-config permissions (640) - Reloads PHP-FPM
Kernel tuning (sysctl.conf, both VMs)
kern.maxfiles=200000
kern.ipc.somaxconn=4096
net.inet.tcp.sendspace=262144
net.inet.tcp.recvspace=262144
Additional on zamolxis:
vfs.zfs.arc_max=2147483648 # cap ZFS ARC at 2GB, leave RAM for PHP-FPM + nginx UBC
Crons (zamolxis root crontab)
*/5 * * * * curl -s -o /dev/null https://sibiuindependent.ro/wp-cron.php?doing_wp_cron
0 3 * * * /root/cache-warmer.sh > /dev/null 2>&1
0 3 * * 1 /root/js-refresh.sh >> /root/js-refresh.log 2>&1
@reboot sleep 60 && /root/cache-warmer.sh > /dev/null 2>&1
Cache Warmer
/root/cache-warmer.sh — reads Yoast sitemap index (89 sub-sitemaps), warms all URLs via HTTP directly to nginx on port 80 (nginx does not serve HTTPS; TLS is terminated at NPM).
#!/bin/sh
DOMAIN="sibiuindependent.ro"
NGINX="127.0.0.1"
CONCURRENCY=6
SITEMAP_INDEX=$(curl -s --max-time 30 \
--resolve "${DOMAIN}:80:${NGINX}" \
"http://${DOMAIN}/sitemap_index.xml")
SITEMAPS=$(echo "$SITEMAP_INDEX" | grep -oE 'https?://[^<]+\.xml' \
| sed "s|https://${DOMAIN}|http://${DOMAIN}|")
URLS=$(for SM in $SITEMAPS; do
curl -s --max-time 30 --resolve "${DOMAIN}:80:${NGINX}" "$SM" \
| grep -oE 'https?://[^<]+' | grep -v '\.xml' \
| sed "s|https://${DOMAIN}|http://${DOMAIN}|"
done)
echo "$URLS" | grep 'http' | xargs -P $CONCURRENCY -I{} sh -c \
'curl -s -o /dev/null --max-time 20 --resolve "'"${DOMAIN}"':80:'"${NGINX}"'" "{}" 2>/dev/null'
JS Asset Refresh
/root/js-refresh.sh — weekly, Monday 03:00:
#!/bin/sh
WEBROOT=/var/www/sibiuindependent.ro
curl -s https://cdn.onesignal.com/sdks/OneSignalSDK.js > ${WEBROOT}/onesignal.js
curl -s https://cdn.onesignal.com/sdks/OneSignalPageSDKES6.js > ${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
DB Migration
Source: transilvan (Debian 12 + MariaDB), socket /tmp/mysqld.sock, SSH port 79.
Destination: decebal (FreeBSD 15 + MySQL 8.4.9), socket /var/db/mysql/mysql.sock.
# Run from zamolxis (has access to both via Tailscale/direct)
ssh -p 79 root@100.99.157.56 \
"mysqldump -S /tmp/mysqld.sock --single-transaction --no-tablespaces \
--routines --triggers --events sql_sibiuindepen" \
| mysql --defaults-file=/tmp/.imp.cnf -f --get-server-public-key sql_sibiuindepen
Notes:
--no-tablespacesinstead of--set-gtid-purged=OFFon MariaDB source (MariaDB does not support that flag)--defaults-filemust be the first argument tomysql-f(force) was needed due to a duplicate key inddf44d_yoast_indexable_hierarchy— Yoast's indexable table had integrity issues, non-critical (regenerates on save)- Final import: 143,266 posts, 121 tables, 1,850MB
Post-Cutover Issues & Fixes
1. S3 images not loading — WPS3Media class not instantiating
Root cause: php85-simplexml not installed. WPS3Media's AWS SDK compat check (AS3CF_Pro_Installer) detected missing SimpleXML and blocked the entire plugin from loading with "no SimpleXML PHP module." No PHP error was emitted — plugin simply never registered its wp_get_attachment_url filter.
Fix: pkg install -y php85-simplexml php85-xmlwriter && service php_fpm restart
Verification: Amazon_S3_And_CloudFront_Pro class now instantiates; URLs rewrite to https://s3.palmasolutions.net/.... 101,145 files tracked in ddf44d_as3cf_items.
2. wp-admin "This has been disabled." — by design
Perfmatters plugin custom login URL feature: login_url: intrarepresa. Both /wp-login.php and /wp-admin are blocked with a 403. Login is at /intrarepresa.
3. Articles redirecting to example.com
Two concurrent causes:
a) Empty permalink_structure — the DB import came from a server that had the structure set, but something reset it on the new DB. Pretty URLs were broken entirely.
Fix: wp --allow-root rewrite structure '/%year%/%monthnum%/%day%/%postname%/' --hard && wp rewrite flush
b) InformatiQ-Toolkit security plugin — was configured with redirect_url: https://example.com/blocked and response_code: 301_custom. Any request the plugin classified as a bot or bad actor got a 301 to example.com, which was then cached by nginx (fastcgi_cache_valid 200 301 302 24h) for 24 hours.
Fix: response_code changed to 403, redirect_url cleared.
Note: nginx's fastcgi_cache_valid should not cache 301/302. Consider changing to fastcgi_cache_valid 200 24h only.
4. Redis Object Cache plugin inactive
The object-cache.php drop-in was installed and functioning (Redis was receiving cache hits). However the plugin itself was marked inactive in WordPress — WP-CLI wp redis commands and the admin UI status panel were non-functional.
Fix: wp --allow-root plugin activate redis-cache
5. DB backup cron never ran
The root crontab had 0 2 * * * root /root/db-backup.sh — with root as a spurious user field (valid in /etc/crontab system format, not in user crontab format). Cron was trying to execute root /root/db-backup.sh as the command, which failed silently. No backups had run since before the migration.
Fix: removed the root field. First successful backup: 137MB, completed in 19 seconds.
6. Redis maxmemory unbounded
Redis maxmemory was 0 (no limit) with noeviction policy. On a server with competing workloads, unbounded Redis with noeviction risks OOM if the object cache grows unexpectedly.
Fix: maxmemory 3gb, maxmemory-policy allkeys-lru, persisted to /usr/local/etc/redis.conf.
7. WP_DEBUG left on after migration
WP_DEBUG = true was generating PHP notices on every uncached request, logged to nginx error log. Overhead on both disk I/O and PHP execution.
Fix: WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY all set to false.
8. nginx static location missing webp
The static file location regex did not include webp. WPS3Media serves WebP variants; without the static location match, .webp requests fell through to PHP-FPM, which loaded WordPress, checked the DB, found nothing local, and generated errors.
Fix: added webp|mp4|webm|ogv to the static location regex.
Current Performance (2026-05-31)
zamolxis
| Metric | Value |
|---|---|
| Load average | 1.76 (6 vCPU) |
| RAM | 24GB, no swap |
| Disk | 10GB / 75GB (14%) |
| nginx cache | 8,288 files, 1.0GB |
| Redis hit rate | 86.9% |
| Redis memory | 1.03GB / 3GB cap |
| OPcache | 99.6%+ hit rate, 3,343 cached scripts |
decebal
| Metric | Value |
|---|---|
| Load average | 0.06 (6 vCPU) |
| RAM | No swap, ~18GB free |
| MySQL DB size | 1,850MB |
| Posts | 143,266 |
| Threads connected | 4 |
| Slow queries | 73 in 2 days |
| Disk (MySQL data) | 3.4GB / 94GB (4%) |
| Backup | 137MB daily, verified |
Deployment Status
| Item | Status |
|---|---|
| zamolxis provisioned and configured | Done |
| decebal provisioned and configured | Done |
| rsync (7.1GB webroot) | Done |
| DB import (143,266 posts, 121 tables) | Done — 2026-05-31 |
| NPM cutover (dracula → zamolxis) | Done — 2026-05-31 |
| Tailscale IPs confirmed | zamolxis: 100.115.128.41 / decebal: 100.67.166.29 |
| NPM Tailscale IP confirmed | 100.89.238.4 |
| pf HTTP rule locked to NPM | Done |
| php85-simplexml installed (WPS3Media) | Done |
| WPS3Media S3 rewrite working | Done — 101,145 files |
| Redis Object Cache plugin active | Done |
| Redis maxmemory capped | Done — 3GB allkeys-lru |
| WP_DEBUG disabled | Done |
| nginx static location includes webp | Done |
| InformatiQ-Toolkit redirect fixed | Done — 403 instead of 301→example.com |
| Permalink structure restored | Done |
| DB backup cron fixed and verified | Done — 137MB/run |
| Temp credential files removed | Done |
| Migration key removed | Done |
| Cache warmer running | Done — @reboot + daily 03:00 |
| Old VMs (dracula, transilvan) | Keep powered, no traffic — decommission after 7 days stable |