Jump to content

NPM Migration to Homelab VPS Relay 260401

From MediawikiCIT

Migrating NGINX Proxy Manager Off a Compromised VPS

A practical runbook for moving NPM to your homelab and turning your VPS into a pure relay.


Background and Lessons Learned

This tutorial documents a real incident where a VPS running NGINX Proxy Manager (NPM) became compromised. The infection vector was a Minecraft server setup tutorial that turned out to be malware — it used the server's open Postfix port to send approximately 28,000 spam emails before being detected.

The core lesson: Never install experimental software directly on a bare server. Use Distrobox containers or Docker for anything you are testing or following a tutorial for. The host OS should only run what you deliberately chose to put there.

The architectural lesson: NPM is surprisingly easy to back up and restore. This means you do not need to keep it on an expensive VPS. A cheap VPS can be reduced to a pure traffic relay, with NPM and all services running on hardware you own at home.


Architecture Overview

Internet → VPS (pure relay, iptables DNAT) → ZeroTier → Home server NPM → your services
Component Role
VPS (e.g. Contabo) Public IP relay only — iptables forwards port 80/443 over ZeroTier
PC03 (primary) Runs NPM + all Docker services
PC02 (warm backup) NPM mirror, ready to take over
Synology NAS Always-on backup repository, watchdog, cold archive

The failover flow is: detect (PC06 watchdog, 3 fails trigger) → wake (PC06 → Synology sends WoL → PC02 boots) → failover (DNAT swaps to PC02 once NPM responds). Restic runs 3×/day to Synology from PC03, and mirrors to PC02 on wake.

Why this works without a static home IP

Your home ISP likely does not give you a static public IP. That does not matter here. Both the VPS and your home servers are connected via ZeroTier, which assigns fixed private IPs regardless of what your ISP does. The VPS always knows where to forward traffic.

Approximate hardware costs (Philippine Peso)

Hardware Cost
VPS (e.g. Contabo, annual) ₱5,000/yr
Dell OptiPlex micro (16GB RAM, 256GB SSD) × 2 ₱10,000 each
Synology NAS with 2 × 3.5TB IronWolf ₱28,000

A system good for 7–10 years, with full redundancy, for roughly what a single mid-tier cloud server costs annually.


Part 1 — Backing Up NPM

Step 1 — Identify your NPM setup

First confirm whether your NPM is using SQLite (simpler) or MariaDB (more common in older installs):

docker ps

If you see a separate mariadb or mariadb-aria container alongside NPM, you are on the MariaDB variant. This tutorial covers that setup.

Find your compose file:

find /root /opt /etc/docker -name "docker-compose.yml" 2>/dev/null

Check where NPM mounts its data:

docker inspect <npm-container-name> | grep -A 20 '"Mounts"'
docker inspect <db-container-name>  | grep -A 20 '"Mounts"'

Note the Source paths — everything NPM needs is in those directories.

Step 2 — Create the backup

Run this as root. Replace /data/compose/3 with your actual mount paths if different.

# Create staging directory
mkdir -p /root/npm-backup/zt

# Back up ZeroTier identity (preserves your ZT IP after reformat)
cp /var/lib/zerotier-one/identity.secret /root/npm-backup/zt/
cp /var/lib/zerotier-one/identity.public  /root/npm-backup/zt/

# Back up the compose file
cp /root/nginx-pm<your-folder>/docker-compose.yml /root/npm-backup/

# Dump the database (do this while containers are running)
docker exec <db-container-name> \
  mysqldump -u npm -pnpm npm > /root/npm-backup/npm-db-dump.sql

# Archive all NPM data (app config, SSL certs, MySQL files)
tar -czvf /root/npm-backup/npm-data.tar.gz /data/compose/3/

# Verify
ls -lh /root/npm-backup/

Check that npm-db-dump.sql is not zero bytes. If it is, the dump failed silently — the raw MySQL files in npm-data.tar.gz will be your restore source instead.

Step 3 — Transfer to your NAS or local machine

rsync -avz --progress \
  /root/npm-backup/ \
  user@<NAS-IP>:/volume1/backups/npm/

Or compress first:

cd /root
tar -czvf npm-backup.tar.gz npm-backup/
rsync -avz --progress npm-backup.tar.gz user@<NAS-IP>:/volume1/backups/npm/

Part 2 — Restoring NPM on a New Machine

All commands run as root. The backup file goes in /root/.

Important: NPM data always restores to the same absolute path (/data/compose/3/) regardless of which machine you are on. This is hardcoded in the compose file. Do not change it.

Step 1 — Extract the backup

cd /root
tar -xzvf npm-backup.tar.gz
ls npm-backup/

You should see: docker-compose.yml, npm-data.tar.gz, npm-db-dump.sql, zt/

Step 2 — Restore data files

# Extract directly to / — this recreates /data/compose/3/ automatically
tar -xzvf /root/npm-backup/npm-data.tar.gz -C /

# Verify
ls /data/compose/3/
# Should show: data  letsencrypt  mysql

Step 3 — Deploy the stack

mkdir -p /root/nginx-pm
cp /root/npm-backup/docker-compose.yml /root/nginx-pm/

cd /root/nginx-pm
docker compose up -d

docker ps

Both the NPM app container and the MariaDB container should show as running.

Step 4 — Verify

Open http://<machine-IP>:81 in a browser. Your proxy hosts, SSL certificates, and settings should all be present exactly as they were.


Part 3 — Setting Up the VPS as a Pure Relay

After reformatting your VPS, the entire setup is:

Step 1 — Install ZeroTier and join your network

curl -s https://install.zerotier.com | bash
zerotier-cli join <your-network-id>

Then go to your ZeroTier Central dashboard, authorize the new node, and assign it your chosen fixed IP (e.g. the same IP it had before).

Note on ZeroTier IP preservation: You do not need to restore the ZeroTier identity files to keep the same IP. Simply authorize the new node in ZT Central and manually assign the same managed IP there. The identity restore is only needed if you specifically want the same node ID.

Contabo UI warning: Contabo's control panel is easy to misclick. Always double-check which server you have selected before clicking reformat. The interface can make it easy to accidentally act on the wrong server.

Step 2 — Enable IP forwarding and set up relay rules

Replace 192.168.196.76 with your home server's ZeroTier IP.

# Enable forwarding
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
sysctl -p

# Forward web traffic to home NPM
iptables -t nat -A PREROUTING -p tcp --dport 80  -j DNAT --to-destination 192.168.196.76:80
iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 192.168.196.76:443
iptables -t nat -A POSTROUTING -j MASQUERADE

# Make rules persistent across reboots
apt install iptables-persistent -y
netfilter-persistent save

Step 3 — Verify

iptables -t nat -L -n -v

You should see packet counters incrementing on the DNAT rules as traffic arrives.


Part 4 — Hardening WordPress Behind NPM

After migrating, a brute force attack on /wp-login.php was detected within hours. Two layers of protection:

Layer 1 — Hide the login URL

Install the WPS Hide Login plugin in WordPress:

  1. WordPress admin → Plugins → Add New → search WPS Hide Login
  2. Install and activate
  3. Settings → WPS Hide Login → set a custom login path (e.g. /your-secret-path)
  4. Save

The default /wp-login.php now returns 404 to everyone.

Layer 2 — Restrict via NPM custom location

In NPM, edit your WordPress proxy host → Custom Locations tab:

Add location: /wp-login.php

  • Forward Hostname/IP: <wordpress-server-IP>
  • Forward Port: <wordpress-port>
  • In the gear/config box, add:
allow 192.168.196.0/24;
deny all;

Repeat for /wp-admin/.

This means even if someone discovers your hidden login URL, they cannot reach it unless they are on your ZeroTier network.


Key Takeaways

On security:

  • Never follow tutorials directly on a bare server. Use Distrobox or Docker to isolate experiments.
  • Check for open mail relays — an open Postfix is a common infection consequence.
  • Bots scan for /wp-login.php within hours of a site going live. Hide it and restrict it immediately.

On operations:

  • NPM is stateless enough to be fully backed up in a single tar file and restored in under 15 minutes.
  • ZeroTier makes home servers first-class infrastructure — a cheap VPS becomes just a public IP rental.
  • Always run docker ps and ss -tlnp before and after a migration to confirm port ownership.
  • When restoring, mind whether you are logged in as root or a regular user — file paths differ and commands like tar -C / require root.

On architecture:

  • A ₱5,000/yr VPS as a pure relay is far more resilient than a ₱15–30k/yr VPS running everything. When it gets compromised again, you reformat it in 15 minutes with nothing to lose.
  • SSL certificate renewals continue to work transparently through the relay — Let's Encrypt ACME challenges on port 80 get forwarded to your home NPM automatically.