Last updated on June 30, 2025
π οΈ Full Migration from LAMP to Hardened NGINX VPS (Debian 12)
After years on a basic LAMP stack (Linux, Apache, MySQL, PHP), I finally migrated both of my WordPress installations β www and dev β to a brand-new, NGINX-based Debian 12 VPS on Linode.
This wasnβt just a file move. It meant building a secure, encrypted server from scratch, manually configuring every service, restoring full UpdraftPlus backups, setting up SSL with Certbot, and resolving dozens of nuanced WordPress edge cases.
This post documents the entire process, including setup, challenges, recovery steps, lessons learned β and every command it took to pull it off.
βοΈ VPS Setup and Hardening
π Server Prep: Initial Setup and Hardening
The first step was launching a fresh Debian 12 VPS with hypervisor-level encryption. My main goal was switching to NGINX, but I also decided to:
- Upgrade from Debian 11 to 12
- Move to an encrypted VPS
- Leave the old server running as a fallback
This was originally a future task, but combining everything made the migration safer and more efficient.
π§ Debian Linux Configuration
sudo apt update
sudo apt install curl unzip nano sudo git htop gnupg lsb-release ca-certificates ufw fail2ban nginx php php-fpm mariadb-server
π SSH Lockdown
Linode added a default RSA key from my account for Lish authentication, but I needed to manually add my own key to allow SSH access and also import my GitHub key for later use.
I displayed my keys on the old VPS using cat, then used nano with sudo on the new server to paste them into place:
sudo nano ~/.ssh/authorized_keys
# (paste in public key)
sudo chmod 700 ~/.ssh
sudo chmod 600 ~/.ssh/authorized_keys
sudo systemctl restart sshd
π System Monitoring & Baselines
fastfetch > ~/stats-before.txt
df -h > disk.txt
free -h > mem.txt
π§± Firewall & Intrusion Prevention
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo systemctl enable --now fail2ban
I also captured full network, service, and system state logs (see command list at end).
π§ PHP, MariaDB, and NGINX Stack
π¦ Core Packages
sudo apt install php php-fpm php-mysql php-curl php-xml php-mbstring php-zip php-gd
sudo systemctl enable php8.2-fpm
sudo systemctl enable mariadb
π NGINX Setup
sudo systemctl enable nginx
sudo ufw allow 'Nginx Full'
sudo systemctl start nginx
π MariaDB Hardening
sudo mysql_secure_installation
Created users and databases manually:
CREATE DATABASE stationaryreality_wp;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON stationaryreality_wp.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
π Directory & Permissions
sudo mkdir -p /var/www/dev.stationaryreality.com /var/www/www.stationaryreality.com
sudo chown -R www-data:www-data /var/www/*
sudo chmod -R 755 /var/www/*
π§± WordPress Installations (Dev & Main)
π οΈ Deployment
cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar -xvzf latest.tar.gz
sudo cp wordpress/* /var/www/dev.stationaryreality.com/
sudo mv wordpress/* /var/www/www.stationaryreality.com/
βοΈ Manual wp-config.php Setup
- Created fresh DB credentials
- Set salts manually
- Adjusted file permissions
sudo cp wp-config-sample.php wp-config.php
sudo nano wp-config.php
π UpdraftPlus Restore + Migration
Dev Site
Restored cleanly with no issues.
Main Site
Multiple issues:
- Incomplete restores
- Corrupt uploads folder (due to Simply Static plugin)
- Database mismatch
- Cloudflare & HTTPS interference
Fixes that saved it:
- Disabled plugins
- Cleaned uploads/ and re-backed up from live VPS
- Forced HTTP-only access in NGINX config to regain admin access
- Enabled plugins one-by-one post-restore
β It’s a big deal that full recovery is possible, especially if the original server is still available for a secondary backup. If not, you might need to manually strip corrupted files from the Updraft zip, though checksums may complicate that. In theory, you could manually upload missing files.
π‘οΈ SSL & NGINX Configuration with Certbot
sudo certbot --nginx -d www.stationaryreality.com -d stationaryreality.com
NGINX Redirect Config:
server {
listen 80;
server_name www.stationaryreality.com stationaryreality.com;
return 301 https://$host$request_uri;
}
π Wordfence & Cloudflare Hardening
- Enabled extended Wordfence firewall
- Resolved multisite plugin conflicts
- Cloudflare: temporarily disabled SSL to prevent Certbot conflicts
# Manually set trusted proxy IPs# (added Cloudflare IPs to Wordfence config)
Free plan allowed max 64% protection.
π€― Notable Challenges
- WordPress database restore order matters
- macOS /etc/hosts parsing is fragile:
#can break spacing - HTTPS redirect loops from mixed content
- Simply Static plugin corrupted backup
- Certbot canβt run on HTTPS until it first validates over HTTP β ironic given SSL expectations
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
β Final Tweaks
- β Plugins re-enabled
- β Wordfence rules finalized
- β Auto-backups: skipped (manual preferred)
π§ Key Takeaways
- Full stack migrations require care, but save money and give full control
- UpdraftPlus is reliable β but only if all pieces are intact
- NGINX and SSL should be configured after WordPress is stable
- Total commands used: ~235
- Keep your old server running until the new one is proven
π§° Full Command Set (Condensed)
System Setup & SSH
sudo apt update
sudo apt install fastfetch curl unzip nano sudo git htop gnupg lsb-release ca-certificates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
sudo hostnamectl set-hostname statrealsrvr
mkdir -p ~/.ssh
sudo cp /root/.ssh/authorized_keys ~/.ssh/
sudo chown -R srdev:srdev ~/.ssh/
sudo chmod 700 ~/.ssh/
chmod 600 ~/.ssh/authorized_keys
sudo nano /etc/ssh/sshd_config
sudo systemctl restart sshd
Baseline Logs
mkdir ~/server_baselines
uname -a >> system_baseline.txt
lsb_release -a >> system_baseline.txt
df -h >> system_baseline.txt
free -h >> system_baseline.txt
uptime >> system_baseline.txt
ip a >> network_baseline.txt
ss -tulnp >> network_baseline.txt
Firewall & Fail2Ban
sudo apt install ufw fail2ban -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo systemctl enable --now fail2ban
sudo nano /etc/fail2ban/jail.local
sudo systemctl restart fail2ban
Install Services
sudo apt install nginx php php-fpm php-mysql php-xml php-curl php-mbstring php-zip php-gd php-cli -y
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl enable php8.2-fpm
sudo systemctl start php8.2-fpm
sudo apt install mariadb-server
sudo systemctl enable mariadb
sudo systemctl start mariadb
sudo mysql_secure_installation
WordPress Install
curl -O https://wordpress.org/latest.tar.gz
tar -xvzf latest.tar.gz
sudo mkdir -p /var/www/dev.stationaryreality.com
sudo mv wordpress/* /var/www/dev.stationaryreality.com/
sudo chown -R www-data:www-data /var/www/dev.stationaryreality.com/
sudo find /var/www/dev.stationaryreality.com/ -type d -exec chmod 755 {} \;
sudo find /var/www/dev.stationaryreality.com/ -type f -exec chmod 644 {} \;
NGINX Site Config
sudo nano /etc/nginx/sites-available/dev.stationaryreality.com
sudo ln -s /etc/nginx/sites-available/dev.stationaryreality.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Database & wp-config
sudo mysql -u root -p
# Inside MySQL:
CREATE DATABASE stationaryreality_wp;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON stationaryreality_wp.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
SSL with Certbot
sudo certbot --nginx -d www.stationaryreality.com -d stationaryreality.com
sudo nginx -t && sudo systemctl reload nginx
WordPress Restore (manual + Updraft)
sudo mv ~/updraft-temp/* /var/www/dev.stationaryreality.com/wp-content/updraft/
sudo chown www-data:www-data /var/www/dev.stationaryreality.com/wp-content/updraft/*
rm -rf /var/www/www.stationaryreality.com/wp-content/plugins/simply-static/
