Migrating to a Proxy-First Architecture Safely
Move a direct-to-internet app behind a reverse proxy in staged steps with minimal downtime, instant rollback, and smoke tests before cutover.
Netbay Engineering
Netbay Engineering
On this page
Most servers were not born proxy-first. Somewhere in the past someone pointed a framework directly at the public internet, and since it worked, it stayed. Migrating to a reverse proxy in front of a live app feels risky — but done in stages it is one of the gentler infra changes you can make. The plan below assumes nothing breaks except your habit of reaching apps on bare ports.
Stage the migration
- **Put the proxy on the box, pointing at the app, on a test port.** Nothing listens on 80 yet; you are validating config, not switching traffic.
- **Verify with Host headers and local curl.** Prove that proxied traffic produces the same responses as direct traffic.
- **Move the app off the public port.** Bind the app to 127.0.0.1 only, so the proxy becomes the sole public entry point.
- **Expose the proxy on 80, then 443 with a certificate.**
- **Point DNS at the proxy and run smoke tests.**
- **Keep the rollback path warm:** the old direct route still configured, just not enabled.
Step by step: from direct to proxied
Start by proving the proxy behaves. With the app still on its public port, add a proxy port that talks to it — a complete side-by-side that risks zero user traffic.
server {
listen 18080;
server_name _;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Then compare direct and proxied responses byte for byte before touching anything live.
curl -s http://127.0.0.1:3000/health > direct.out
curl -s -H "Host: app.example.com" http://127.0.0.1:18080/health > proxied.out
diff direct.out proxied.out && echo OKA clean diff is your green light. The moment one response differs — a missing header, a mangled absolute URL — you fix it now, while both paths still exist.
The cutover
Rebind the app to localhost, point the proxy at it, move the proxy onto 80, and add TLS. Then descend any NAT or firewall rules.
sed -i 's/0.0.0.0:3000/127.0.0.1:3000/' /etc/app/app.conf
systemctl restart app
nginx -t && systemctl reload nginx
ufw allow 80/tcp
ufw allow 443/tcp
ufw delete allow 3000/tcpThe sed line tweaks whichever file actually binds the app; the order matters — close the old public port only after the proxy is answering on 80 and 443. Requests in flight for the old port fail for the duration of the reload, so schedule each step for an off-peak window.
Smoke tests and the rollback runbook
After the cutover, run the same assertions a user would and check that the proxy is really the only entrance.
curl -sfI https://app.example.com/ | head -n 3
curl -s 127.0.0.1:3000/health -o /dev/null -w "%{http_code}
" # expect 000
curl -s http://127.0.0.1:443/health -o /dev/null -w "%{http_code}
"The middle line confirms the app no longer answers on the public port, which is the whole point. Write the rollback steps down before you start: restore the app's public bind, reload, re-open the firewall port, and remove the proxy from 80. A rollback that requires thinking is a rollback you will not perform.
Takeaway
The safest migration is a reversible one: proxy beside the app, diff the responses, rebind the app to localhost, cut ports over, and leave the rollback runbook written down. Done this way, the proxy-first change is boring on purpose. A fresh Netbay VPS is the ideal rehearsal box — run the whole sequence once for practice, then again in production, at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance