Back to tutorials
Tutorial

Nginx reverse proxy tutorial (2026): Put Nginx in front of WordPress on a VPS for speed, HTTPS, and safer origins

Nginx reverse proxy tutorial: front WordPress with HTTPS, caching basics, real IPs, and safe headers on a VPS in 2026.

By Anurag Singh
Updated on Jul 28, 2026
Category: Tutorial
Share article
Nginx reverse proxy tutorial (2026): Put Nginx in front of WordPress on a VPS for speed, HTTPS, and safer origins

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 (A record, and AAAA if 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-Proto and the wp-config.php HTTPS 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 apache2 and increase proxy_read_timeout for long requests.
  • Wrong client IP for rate limits/WAF: Add mod_remoteip (Apache) or real_ip config (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 -t passes.
  • 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.

Nginx reverse proxy tutorial (2026): Put Nginx in front of WordPress on a VPS for speed, HTTPS, and safer origins | HostMyCode