- nginx real_ip_header X-Real-IP: access log and PHP now see true client IPs - pf: HTTP open to all (WAF on NPM handles filtering) - sibiu-independent-tweaks: si_purge_nginx_homepage_cache() hook deletes homepage fastcgi cache file on post publish/unpublish — fixes stale content for anonymous (mobile) users - SSH config: updated to direct Tailscale IPs, removed old migration proxy - sibiuindependent.ro.MD: documented issues 9-11 and fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
25 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 open to all (WAF on NPM), SSH only from mgmt + Tailscale |
| Real IP | nginx real_ip_header X-Real-IP from NPM — access log and plugins see true client IPs |
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.
9. Login blocked for all users — ASE Limit Login Attempts locked NPM's IP
All traffic passes through NPM (Tailscale 100.89.238.4). nginx passed REMOTE_ADDR = 100.89.238.4 to PHP. The Admin Site Enhancements plugin's Limit Login Attempts feature tracked all failed logins against this single IP. After multiple users tried and failed, the 24h lockout triggered — blocking everyone from /intrarepresa.
Fix (immediate): ASE deactivated.
Fix (permanent): nginx real_ip_header X-Real-IP + set_real_ip_from 100.89.238.4 configured. $remote_addr now reflects true client IP from NPM's X-Real-IP header. Access log and all PHP plugins see real per-user IPs. If ASE is re-enabled, Limit Login Attempts will work correctly per-user.
10. pf HTTP restriction opened — WAF on NPM handles filtering
HTTP was restricted to NPM's Tailscale IP only. Opened to all: pass in proto tcp from any to any port 80 keep state. WAF rules on NPM handle filtering upstream.
11. Stale nginx cache on homepage for anonymous (mobile) users
No cache invalidation on post publish. Anonymous users (typically mobile) got the nginx-cached homepage for up to 24h after new posts were published. Logged-in users (desktop) bypassed cache via wordpress_logged_in cookie.
Fix: si_purge_nginx_homepage_cache() hook added to sibiu-independent-tweaks/sb.php. On transition_post_status (publish/unpublish), it calculates the homepage cache file path (MD5 of httpGETsibiuindependent.ro/, nginx levels=1:2) and deletes it. Next organic request regenerates the cache. PHP www user has write access to nginx cache files (same owner).
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 |