
A reverse proxy helps you keep one VPS organized. You get a single public entry point, clean routing for multiple sites, and consistent TLS. You also get fewer “where is this traffic going?” moments. This VPS reverse proxy setup tutorial shows how to put Nginx in front on Ubuntu/Debian and route requests to Apache, PHP-FPM, Node apps, or WordPress—without building an overcomplicated stack.
You’ll set it up so it stays maintainable as you add domains or migrate services. Expect a predictable file layout, real client IPs in logs, safe default headers, and a repeatable per-domain checklist.
What you’ll build (and why it matters on a hosting VPS)
Here’s the target design:
- Nginx listens on ports 80/443 and terminates TLS.
- Each domain gets its own server block and upstream definition.
- Backends run privately (127.0.0.1 or a private VLAN IP), keeping your app ports off the internet.
- Logs record the real client IP (not just 127.0.0.1), so rate limiting and incident response actually work.
This layout fits “WordPress plus a separate app,” small resellers hosting a few properties on one VPS, and teams moving off shared hosting into a stack they can control.
If you want a predictable Linux baseline, start with a VPS where you control the firewall, kernel updates, and package versions.
A HostMyCode VPS fits reverse proxy work well because you can tune Nginx, pin versions, and keep the backends private.
Prerequisites and a quick compatibility checklist
- Ubuntu 24.04 LTS / 26.04 LTS or Debian 12/13 (commands below assume Ubuntu/Debian).
- A domain (or several) pointed at your VPS public IP.
- SSH access as a sudo user.
- Backends already running (Apache on 127.0.0.1:8080, Node on 127.0.0.1:3000, PHP-FPM socket, etc.), or you can stand up a simple test backend.
Common pitfall: proxying to a backend that still binds to a public interface.
Keep backends on localhost/private IPs, and make Nginx the only public listener.
Step 1: Install Nginx and open only the ports you need
Install Nginx:
sudo apt update
sudo apt install -y nginx
Confirm it runs:
systemctl status nginx --no-pager
If you manage your firewall with UFW, allow only SSH and web ports:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose
If you’re tightening baseline security at the same time, HostMyCode’s VPS firewall setup guide covers safe SSH rules and how to layer a cloud firewall cleanly.
Step 2: Create a clean file layout for multi-site reverse proxy configs
On Ubuntu/Debian, Nginx uses:
/etc/nginx/sites-available/for stored vhost configs/etc/nginx/sites-enabled/for enabled symlinks
It also helps to keep reusable bits in snippets.
Reserve a single ACME webroot for challenges.
sudo mkdir -p /etc/nginx/snippets
sudo mkdir -p /var/www/_acme-challenge
Create a baseline proxy snippet:
sudo nano /etc/nginx/snippets/proxy-common.conf
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Avoid proxy buffering surprises for APIs / SSE (tune per app)
proxy_buffering on;
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
Add a minimal security headers snippet. Keep it conservative.
Some apps break on overly strict defaults.
sudo nano /etc/nginx/snippets/security-headers-basic.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
If you want a stronger header set (HSTS/CSP), apply it per domain and test.
The cPanel-focused advice still translates well conceptually: security headers setup guide.
Step 3: Define upstream backends (Apache, Node, or PHP-FPM)
Upstreams keep your vhost files stable.
If a service moves from port 3000 to 3001 later, you change one place.
Create an upstreams file:
sudo nano /etc/nginx/conf.d/upstreams.conf
# Apache backend (configured to listen on 127.0.0.1:8080)
upstream apache_backend {
server 127.0.0.1:8080;
keepalive 32;
}
# Node app backend
upstream node_app {
server 127.0.0.1:3000;
keepalive 32;
}
Apache note: if Apache currently listens on 80, move it to 8080 (or 127.0.0.1:8080).
On Ubuntu, you’ll usually change:
/etc/apache2/ports.conf/etc/apache2/sites-available/*.conf
A minimal change looks like this:
# /etc/apache2/ports.conf
Listen 127.0.0.1:8080
Then restart Apache:
sudo systemctl restart apache2
Step 4: Build your first Nginx reverse proxy vhost (HTTP only)
Start with plain HTTP. Confirm routing first.
Add TLS and redirects after that.
sudo nano /etc/nginx/sites-available/example.com.conf
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
# ACME challenge path for Let's Encrypt
location ^~ /.well-known/acme-challenge/ {
root /var/www/_acme-challenge;
default_type "text/plain";
}
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://apache_backend;
}
include /etc/nginx/snippets/security-headers-basic.conf;
}
Enable the site and reload:
sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Quick diagnostic:
curl -I http://example.com
If you see a 502, Nginx can’t reach the upstream.
Check the listener and the error log:
ss -ltnp | egrep ':8080|:3000'
sudo tail -n 50 /var/log/nginx/example.com.error.log
Step 5: Add TLS with Let’s Encrypt (Certbot) without breaking routing
Install Certbot and the Nginx plugin:
sudo apt install -y certbot python3-certbot-nginx
Issue a certificate. Certbot can edit your server blocks automatically.
For a controlled setup, it’s often cleaner to generate the cert and wire the 443 block yourself.
sudo certbot certonly --nginx -d example.com -d www.example.com
Now update the vhost to add a 443 server block and redirect HTTP to HTTPS.
sudo nano /etc/nginx/sites-available/example.com.conf
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/_acme-challenge;
default_type "text/plain";
}
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Safe, modern defaults. Keep simple unless you have a compliance requirement.
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
access_log /var/log/nginx/example.com.ssl.access.log;
error_log /var/log/nginx/example.com.ssl.error.log;
location / {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://apache_backend;
}
include /etc/nginx/snippets/security-headers-basic.conf;
}
Test and reload:
sudo nginx -t
sudo systemctl reload nginx
Renewal check:
sudo certbot renew --dry-run
If renewals fail later, troubleshoot in this order: DNS → port 80 reachability → challenge location.
This walkthrough is a good reference: Let’s Encrypt renewal troubleshooting.
Step 6: Add a second site (different backend) on the same VPS
This is where the pattern pays off.
Each domain gets its own file, while Nginx stays the single front door.
Example: route app.example.net to a Node service on 127.0.0.1:3000:
sudo nano /etc/nginx/sites-available/app.example.net.conf
server {
listen 80;
listen [::]:80;
server_name app.example.net;
location ^~ /.well-known/acme-challenge/ {
root /var/www/_acme-challenge;
default_type "text/plain";
}
location / {
include /etc/nginx/snippets/proxy-common.conf;
# WebSocket support (safe to include even if not used)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://node_app;
}
}
Enable it and get a cert:
sudo ln -s /etc/nginx/sites-available/app.example.net.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certonly --nginx -d app.example.net
Then add a 443 block the same way as your first domain.
Step 7: Preserve real client IPs (and avoid misleading logs)
With Nginx as the public entry point, the backend often “sees” Nginx as the client.
That breaks rate limits, audit trails, and security plugins that rely on IP addresses.
You already send X-Real-IP and X-Forwarded-For. Next, tell your backend to trust those headers.
Only trust them when the request comes from your proxy.
That usually means localhost or your private interface.
Apache (Ubuntu/Debian): enable mod_remoteip:
sudo a2enmod remoteip
sudo nano /etc/apache2/conf-available/remoteip.conf
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1
# If Nginx is on a private interface, add that too:
# RemoteIPTrustedProxy 10.0.0.10
sudo a2enconf remoteip
sudo systemctl restart apache2
Quick check: load a page, then confirm Apache logs show your real IP (not 127.0.0.1).
Step 8: Add basic DDoS/bot friction with Nginx rate limiting (carefully)
Rate limiting won’t stop a serious attack.
It does cut down on noisy scans and aggressive bots.
Keep it targeted. Apply it to login and other sensitive endpoints, not your entire site.
Add to /etc/nginx/nginx.conf inside the http {} block:
sudo nano /etc/nginx/nginx.conf
# 10 requests/second per IP with small burst
limit_req_zone $binary_remote_addr zone=login_zone:10m rate=10r/s;
Then in a site’s server block:
location = /wp-login.php {
limit_req zone=login_zone burst=20 nodelay;
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://apache_backend;
}
Pitfall: rate limiting checkout/cart endpoints for WooCommerce can trigger false positives.
Start with login and XML-RPC (if enabled), then watch your logs.
Step 9: Tighten timeouts and buffers to reduce 502/504 errors
Most 502/504 complaints come from mismatched timeouts between Nginx and the upstream.
Don’t guess. Check how your app behaves during slow requests or background work.
- 502: upstream connection failed (app down, wrong port, firewall, or crashed worker)
- 504: upstream too slow for current proxy timeouts
For slow admin tasks (imports, plugin updates), raise read timeouts only where you need them:
location /wp-admin/ {
include /etc/nginx/snippets/proxy-common.conf;
proxy_read_timeout 180s;
proxy_pass http://apache_backend;
}
If the VPS is under memory pressure, Nginx errors are often the visible symptom.
This performance triage guide helps you confirm CPU/RAM pressure quickly: VPS performance troubleshooting tutorial.
Step 10: Validate your setup with a practical test plan
Run these from your laptop and from the VPS.
You’ll spot most configuration issues in minutes.
- Nginx config sanity:
sudo nginx -t - Certificate and redirects:
curl -I http://example.comandcurl -I https://example.com - HTTP/2 negotiated:
curl -I --http2 https://example.com - Backend reachability:
curl -I http://127.0.0.1:8080(from VPS) - Log tailing:
sudo tail -f /var/log/nginx/example.com.ssl.error.log - DNS correctness:
dig +short A example.com
If you’re moving DNS during a cutover, drop TTL ahead of time and write down a rollback plan.
HostMyCode’s DNS cutover tutorial matches this reverse proxy approach nicely.
Step 11: Common reverse proxy problems (and fast fixes)
Problem: Infinite redirects after enabling HTTPS
- Cause: backend thinks the request is HTTP and redirects to HTTPS repeatedly.
- Fix: ensure
X-Forwarded-Protois set (we did), and configure the app/framework to trust proxy headers.
If you’re debugging WordPress or mixed stacks, this guide speeds things up: HTTPS redirect troubleshooting.
Problem: 413 Request Entity Too Large on uploads
- Fix in Nginx server block:
client_max_body_size 64m; - Also check: PHP
upload_max_filesizeandpost_max_size, plus the app’s own limits.
Problem: 502 Bad Gateway only for one domain
- Cause: wrong upstream, wrong port, or backend bind address.
- Fix: verify backend bind with
ss -ltnp, then testcurlto the backend locally.
Problem: Wrong client IP in app logs/security tools
- Fix: trust
X-Forwarded-Foronly from Nginx IP (127.0.0.1/private IP). Enable Apachemod_remoteipor equivalent in your stack.
Operational checklist: keep the reverse proxy reliable month after month
- Patch cadence: update Nginx and OpenSSL regularly; schedule reboots if kernel updates land.
- Renewals: run
certbot renew --dry-runafter any firewall/DNS changes. - Log rotation: confirm
/etc/logrotate.d/nginxexists; large access logs can fill disks. - Backups: treat
/etc/nginxas critical config; back it up with the rest of your site/app data. - Change discipline: always
nginx -tbefore reload; use version control for/etc/nginxif you can.
If you want a restore process you’ve actually tested, run a drill a few times a year: VPS restore drill tutorial.
If you’re hosting multiple sites or apps behind one clean HTTPS entry point, start with a VPS that has real headroom and predictable I/O. HostMyCode offers VPS hosting if you want full control, and managed VPS hosting if you want someone else handling the baseline maintenance while you focus on your sites.
FAQ
Do I still need Apache if I use Nginx as a reverse proxy?
No. Apache is just one common backend.
You can proxy to Node, Python apps, or Nginx/PHP-FPM directly. The reverse proxy pattern stays the same.
Should Nginx and the backend run on the same VPS?
For small-to-mid hosting setups, yes. Keep backends on 127.0.0.1 or a private interface.
As traffic grows, you can move backends to separate VPS nodes and keep Nginx as the front door.
Can one TLS certificate cover multiple domains?
Yes. Use SAN certificates (-d domain1 -d domain2) or separate certificates per vhost.
Separate certs are usually simpler operationally.
What’s the safest way to test changes without downtime?
Stage config edits, run nginx -t, then systemctl reload nginx.
Reload is graceful; it doesn’t drop active connections the way a full restart can.
Is a reverse proxy worth it for a single WordPress site?
Often yes if you plan to add a second site, an app endpoint, or want standardized TLS and logging.
If you truly have one small site, it may be simpler to run a single web stack until you outgrow it.
Summary: a repeatable reverse proxy pattern you can keep
You now have an Nginx front-end that routes multiple domains to different backends, handles TLS cleanly, and preserves real client IPs for logs and security tools.
With sensible limits and monitoring, the same pattern scales from “two sites on a small VPS” to a multi-tenant hosting node.
If you’re ready to run this on a VPS with enough memory and predictable storage, deploy it on a HostMyCode VPS, or choose managed VPS hosting if you want help keeping the baseline secure and maintained.