
Your WordPress site can feel “slow” even when PHP and MySQL look healthy. The drag is often at the edge: TLS handshakes, static files, keep-alives, and noisy clients hitting your front door. This Nginx reverse proxy tutorial shows how to put Nginx in front of a WordPress origin on a VPS. You’ll terminate HTTPS cleanly, serve assets faster, and keep the origin out of direct reach.
The steps below target Ubuntu Server 24.04 LTS (still a common default in 2026 hosting fleets). This approach works with Apache, Nginx+PHP-FPM, or a control panel stack. The key requirement is simple: the origin must listen on a private port.
What you’ll build (and what you won’t)
You’ll set up a simple two-tier layout on a single VPS:
- Nginx (proxy) listens on ports 80/443 and handles TLS, headers, and basic caching behavior.
- WordPress origin listens on
127.0.0.1:8080(or another private port). It doesn’t need to bind to the public interface.
You are not building a multi-node load balancer here. Keep it boring.
Boring stays online.
Prerequisites checklist (5 minutes)
- An Ubuntu 24.04 VPS with a public IPv4 (and IPv6 if you use it).
- A domain pointing to your VPS (
Arecord, andAAAAif applicable). - Root or sudo access.
- Your WordPress site already working on the origin (even if it’s currently on port 80).
If you’re still deciding where to run this, a HostMyCode VPS fits this pattern well. You get full control over Nginx, firewall rules, and TLS automation.
You also avoid the constraints of shared hosting.
Step 1 — Install Nginx and baseline tools
Update packages and install Nginx plus a few utilities you’ll use for testing:
sudo apt update
sudo apt -y install nginx curl gnupg2 ca-certificates lsb-release
Confirm Nginx is running:
systemctl status nginx --no-pager
curl -I http://127.0.0.1
If curl returns an Nginx header, you’re set.
Step 2 — Put your WordPress origin behind a private port
The proxy only helps if the origin isn’t directly exposed. The goal is simple: public traffic hits Nginx, and Nginx talks to the origin over loopback.
Option A: Origin is Apache
Edit Apache so it listens on 127.0.0.1:8080 instead of :80.
/etc/apache2/ports.conf- Your site vhost in
/etc/apache2/sites-available/
sudo sed -i 's/^Listen 80/Listen 127.0.0.1:8080/' /etc/apache2/ports.conf
Then update your VirtualHost from *:80 to 127.0.0.1:8080.
You can use *:8080 if you prefer, but loopback-only is tighter:
<VirtualHost 127.0.0.1:8080>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
# ...
</VirtualHost>
Restart Apache and test locally:
sudo systemctl restart apache2
curl -I http://127.0.0.1:8080
Option B: Origin is Nginx + PHP-FPM already
If WordPress already runs on Nginx, you can keep it as the origin and move it to 127.0.0.1:8080. You can also replace it.
On a single VPS, replacing the origin Nginx is often simpler. If your current origin config is full of special cases, leaving it in place may be safer.
Change the origin Nginx server to:
listen 127.0.0.1:8080;
Reload and test:
sudo nginx -t
sudo systemctl reload nginx
curl -I http://127.0.0.1:8080
Important: If you’re tempted to use one Nginx instance as both “proxy” and “origin,” don’t. It’s doable, but circular configs and confusing logs are common. On a single VPS, a practical split is Nginx as the proxy and Apache as the origin.
Step 3 — Configure the Nginx reverse proxy server block
Create an Nginx site config for your domain:
sudo nano /etc/nginx/sites-available/example.com
Paste this baseline config (swap example.com and the origin port if needed):
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# Let’s Encrypt will use this path for HTTP-01 challenges
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# TLS certs will be added by Certbot later
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Keep logs per-site so troubleshooting stays sane
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log warn;
# Basic security headers (safe defaults)
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;
# Don’t gzip twice if your origin does compression
gzip off;
# Static assets: serve from origin, but cache at the edge
location ~* \.(css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2?)$ {
proxy_pass http://127.0.0.1:8080;
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 https;
expires 14d;
add_header Cache-Control "public, max-age=1209600";
}
location / {
proxy_pass http://127.0.0.1:8080;
# Preserve host + scheme for WordPress
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
# Real client IPs for logs, rate limits, and WordPress plugins
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support (rare for WordPress, but harmless)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Timeouts that match real hosting traffic
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Don’t let the origin reveal itself
proxy_hide_header X-Powered-By;
}
}
Enable the site and create the ACME webroot folder:
sudo mkdir -p /var/www/letsencrypt
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx
Step 4 — Issue Let’s Encrypt certificates (HTTPS termination)
Install Certbot with the Nginx plugin:
sudo apt -y install certbot python3-certbot-nginx
Request the certificate:
sudo certbot --nginx -d example.com -d www.example.com
Certbot will update your server block and set up renewal timers. Verify renewal works:
sudo certbot renew --dry-run
If you want a deeper renewal troubleshooting flow, keep this handy: TLS certificate renewal troubleshooting tutorial.
Step 5 — Fix WordPress to trust the proxy (HTTPS + real IP)
Two issues show up fast in reverse-proxy setups:
- WordPress generates
http://URLs because the origin only sees HTTP. - Your logs show the proxy IP instead of the real visitor.
Set WordPress to recognize HTTPS behind a proxy
Edit wp-config.php (typically /var/www/…/wp-config.php) and add:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
If you hardcode the site URLs (common in migrations), make sure they’re HTTPS:
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
Ensure the origin logs real client IPs
If Apache is the origin, enable and configure mod_remoteip:
sudo a2enmod remoteip
Create a small config:
sudo nano /etc/apache2/conf-available/remoteip.conf
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1
Enable it and restart Apache:
sudo a2enconf remoteip
sudo systemctl restart apache2
After this, Apache access logs should show visitor IPs instead of 127.0.0.1.
Step 6 — Lock down ports with a firewall (don’t expose the origin)
Your origin should not accept public traffic on 8080. Keep only SSH and Nginx open.
If you use UFW, this baseline keeps SSH working. It also won’t interfere with renewals:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
If you want the full “hosting-safe” UFW walkthrough (including DNS and mail edge cases), use: UFW firewall setup tutorial.
Step 7 — Add caching carefully (start with static, then consider full-page)
Static caching is the easy win. You already added asset caching headers in the Nginx config above.
That’s usually where the obvious speed-up comes from.
Full-page caching is a different category. It can break carts, logins, and personalized pages if you enable it blindly.
If you need full-page caching for WordPress, consider putting Varnish in front (and keep purge rules explicit). This guide stays practical and doesn’t hand-wave the edge cases: Varnish cache setup tutorial.
Step 8 — Quick diagnostics: prove the proxy is doing the right thing
These checks catch most reverse proxy mistakes before they turn into a long night.
1) Confirm HTTPS redirect + HSTS decision
curl -I http://example.com
You should see 301 and a Location: https://example.com/….
Hold off on HSTS until you’ve confirmed all subdomains are HTTPS-ready.
2) Confirm the origin is not reachable publicly
From your laptop, this should fail (or time out):
curl -I http://YOUR_SERVER_IP:8080
3) Confirm WordPress believes it’s HTTPS
In wp-admin, check Settings → General. URLs should be https://.
Then open DevTools and confirm mixed content warnings are gone.
4) Confirm real IPs are logged
Hit the site once, then check logs:
sudo tail -n 20 /var/log/nginx/example.com.access.log
sudo tail -n 20 /var/log/apache2/access.log
Common pitfalls (and how to fix them fast)
- Redirect loop: Usually WordPress forces HTTPS while Nginx forwards as HTTP. Fix by setting
X-Forwarded-Protoand thewp-config.phpHTTPS snippet. - Mixed content: Some plugins hardcode HTTP URLs. Update the site URL, then search/replace in the DB using a safe tool (or a migration plugin) and clear caches.
- 502/504 errors: Origin is down or timeouts too low. Check
systemctl status apache2and increaseproxy_read_timeoutfor long requests. - Wrong client IP for rate limits/WAF: Add
mod_remoteip(Apache) orreal_ipconfig (Nginx origin).
If you’re seeing persistent 502/504s under load, this companion guide walks through the usual causes in a clean order: VPS troubleshooting tutorial (502/504).
Operational checklist (print this before you change anything)
- Origin bound to
127.0.0.1(or protected by firewall). - Nginx has per-site logs and
nginx -tpasses. - Let’s Encrypt renewal dry-run succeeds.
- WordPress recognizes HTTPS behind the proxy.
- Real client IPs show in origin logs.
- UFW (or your firewall) only allows 22/80/443 inbound.
Summary: a reverse proxy that’s actually useful in hosting
This layout keeps your WordPress origin quieter and your edge behavior predictable. Nginx handles TLS and asset caching.
The origin stops taking random internet traffic. You also get one obvious choke point for logging and debugging.
If you want this setup without juggling noisy neighbors or shared-host limits, run it on a managed VPS hosting plan.
Updates, monitoring, and rollback planning should be treated like production work.
If you’re migrating from shared hosting and want a safer cutover, HostMyCode can also handle the move via HostMyCode migrations.
If you’re building a WordPress stack that needs predictable performance and clean control over TLS, a VPS is a solid starting point. Choose a HostMyCode VPS for full root access, or pick managed VPS hosting if you want help keeping the proxy, firewall, and renewals stable.
FAQ
Do I need a reverse proxy for WordPress on a VPS?
Not always. For a single low-traffic site, one web server is often enough. A reverse proxy pays off when you want cleaner TLS handling, faster static delivery, and an origin that isn’t directly reachable.
Should I use HTTP/2 or HTTP/3 here?
HTTP/2 is a safe default on Nginx and is already enabled in the config above. HTTP/3 (QUIC) can help on poor networks, but it adds operational overhead. Only enable it if you plan to test and monitor it.
Will this break WordPress login or wp-admin?
It shouldn’t. The usual culprits are an HTTPS redirect loop (fixed by X-Forwarded-Proto + the WordPress HTTPS snippet) or overly aggressive full-page caching. Start with static-only caching first.
Can I run this in front of cPanel?
Yes, but you need to route carefully and preserve client IPs. If your goal is Nginx in front of Apache/cPanel specifically, follow a cPanel-aware guide like this: VPS reverse proxy setup guide (Nginx + cPanel).
What’s the safest next improvement after this tutorial?
Add log monitoring and alerts so you see proxy/origin failures early. Start with access/error logs and basic resource alerts. Then tighten rate limits and bot blocking only after you’ve profiled legitimate traffic.