How to Configure Nginx as a Reverse Proxy for WordPress in a Subdirectory

Running WordPress behind an Nginx reverse proxy requires more than simply forwarding requests to another server. When the site is published under a subdirectory such as /news, the proxy must preserve the request path, pass the original protocol and hostname, and safely restore the visitor’s real IP address.

This guide explains how to expose a WordPress installation at:

https://example.com/news/

while WordPress is hosted on a separate backend server.

Architecture Overview

This setup uses two servers.

Server A: Public Reverse Proxy

  • Accepts public HTTP and HTTPS traffic
  • Terminates TLS connections
  • Redirects HTTP traffic to HTTPS
  • Proxies requests under /news/ to Server B
  • Passes the original hostname, protocol, and client IP information

Server B: WordPress Backend

  • Hosts the WordPress files
  • Runs Nginx and PHP-FPM
  • Accepts traffic from Server A
  • Restores the real client IP from trusted proxy headers
  • Recognizes that the original request used HTTPS

The public WordPress URL will be:

https://example.com/news/

This guide assumes that WordPress is installed under the /news directory on Server B. For example:

/var/www/example.com/news/

Important Nginx Proxy Path Rule

Nginx handles proxy_pass differently depending on whether the upstream address includes a URI path.

For this setup, use the following configuration:

location /news/ {
    proxy_pass http://wordpress_backend;
}

Because the proxy_pass directive does not include an additional URI, Nginx forwards the complete original request path.

For example, this public request:

/news/wp-admin/

is forwarded to Server B as:

/news/wp-admin/

This avoids accidental path replacement caused by inconsistent trailing slashes in the location and proxy_pass directives.

Step 1: Configure Nginx on Server A

Create or edit the Nginx virtual host configuration for example.com.

Depending on your Linux distribution, the configuration may be stored in one of these locations:

/etc/nginx/conf.d/example.com.conf

or:

/etc/nginx/sites-available/example.com

Define the WordPress Backend

Using an upstream block makes the proxy configuration easier to maintain.

upstream wordpress_backend {
    server B_SERVER_IP:80;
    keepalive 16;
}

Replace B_SERVER_IP with the private or public IP address of Server B.

Whenever possible, use a private network address so that traffic between the two servers does not travel over the public Internet.

Redirect HTTP Traffic to HTTPS

server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    return 301 https://example.com$request_uri;
}

This redirects all HTTP requests to the canonical HTTPS hostname while preserving the original URI.

Configure the HTTPS Reverse Proxy

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com/private.key;

    client_max_body_size 64m;

    location = /news {
        return 301 /news/;
    }

    location /news/ {
        proxy_pass http://wordpress_backend;

        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header Connection "";

        proxy_connect_timeout 10s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 8 16k;
        proxy_busy_buffers_size 32k;
    }

    add_header Strict-Transport-Security "max-age=31536000" always;
}

Understanding the Proxy Headers

Preserve the Public Hostname

proxy_set_header Host $host;

This preserves the public hostname, such as example.com.

WordPress uses the hostname when generating redirects, canonical URLs, administration URLs, media URLs, and other links.

Forward the Original Protocol

proxy_set_header X-Forwarded-Proto $scheme;

This tells Server B whether the original visitor used HTTP or HTTPS.

Although Server A may connect to Server B over HTTP, the browser connection can still use HTTPS. WordPress must know the original protocol to avoid redirect loops, insecure cookies, and mixed-content URLs.

Forward the Client IP Address

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

X-Real-IP contains the address that connected directly to Server A.

X-Forwarded-For maintains the complete proxy chain by appending the current client IP to any existing forwarded addresses.

Server B must trust these headers only when the request comes from Server A.

Why proxy_redirect Is Usually Unnecessary

Older reverse-proxy examples often include a directive similar to this:

proxy_redirect http://B_SERVER_IP/news/ /news/;

This rule is normally unnecessary when:

  • The original Host header is preserved
  • WordPress home and siteurl use the public HTTPS URL
  • WordPress correctly detects the forwarded HTTPS protocol

It is generally better to correct URL generation at the WordPress and proxy-header level than to rewrite backend redirects manually.

If Server B still returns redirects containing its private IP address or internal hostname, verify the WordPress URL settings and forwarded headers before adding a custom proxy_redirect rule.

Test and Reload Nginx on Server A

sudo nginx -t

If the configuration test succeeds, reload Nginx:

sudo systemctl reload nginx

A reload is normally preferable to a restart because it applies the new configuration without abruptly terminating active connections.

Step 2: Configure Real Client IP Handling on Server B

Without additional configuration, Server B sees Server A as the direct client.

Use the Nginx Real IP module to restore the original visitor IP address safely.

Verify the Nginx Real IP Module

nginx -V 2>&1 | grep -o 'http_realip_module'

If the module is available, the command should return:

http_realip_module

Most standard Nginx packages include this module.

Trust Only Server A

Add the following directives inside the Nginx http block on Server B:

http {
    set_real_ip_from A_SERVER_IP;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    # Other Nginx configuration...
}

Replace A_SERVER_IP with the address that Server A uses to connect to Server B.

For example:

set_real_ip_from 10.10.0.10;

If the servers communicate over IPv6, add the trusted IPv6 address as well:

set_real_ip_from 2001:db8::10;

Do Not Trust All IP Addresses

Avoid unrestricted configurations such as:

set_real_ip_from 0.0.0.0/0;

If Server B is reachable by untrusted clients, an attacker could submit a forged X-Forwarded-For header and impersonate another IP address.

Only Server A and other known reverse proxies should be permitted to supply trusted client IP headers.

How real_ip_recursive Works

real_ip_recursive on;

When multiple trusted proxies are involved, this setting instructs Nginx to examine the forwarded IP chain and select the last address that does not belong to a trusted proxy.

It is also safe in a single-proxy architecture when the trusted proxy list is configured correctly.

Step 3: Configure Nginx on Server B

Because Server A preserves the original Host header, the backend virtual host should recognize example.com.

A basic WordPress backend configuration may look like this:

server {
    listen 80;

    server_name example.com;

    root /var/www/example.com;
    index index.php index.html;

    client_max_body_size 64m;

    location = /news {
        return 301 /news/;
    }

    location /news/ {
        try_files $uri $uri/ /news/index.php?$args;
    }

    location ~ ^/news/.*\.php$ {
        try_files $uri =404;

        include fastcgi_params;

        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;

        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    }

    location ~* ^/news/.*\.(?:css|js|jpg|jpeg|gif|png|svg|webp|ico|woff|woff2|ttf)$ {
        expires 7d;
        access_log off;
        try_files $uri =404;
    }
}

Adjust the PHP-FPM socket for the PHP version installed on Server B.

Common examples include:

/run/php/php8.3-fpm.sock
/run/php/php8.4-fpm.sock

PHP-FPM may also listen on a TCP port:

fastcgi_pass 127.0.0.1:9000;

Expected WordPress Directory Structure

With this configuration, WordPress should be installed at:

/var/www/example.com/news/

The WordPress front controller should therefore be:

/var/www/example.com/news/index.php

Test and Reload Nginx on Server B

sudo nginx -t
sudo systemctl reload nginx

Step 4: Make WordPress Recognize Forwarded HTTPS

Server B receives an HTTP request from Server A even though the visitor connected to Server A over HTTPS.

Without additional handling, WordPress may believe that the request is insecure. This can cause:

  • Infinite HTTPS redirect loops
  • HTTP administration URLs
  • Mixed-content warnings
  • Incorrect canonical URLs
  • Login and cookie problems

Add the following code to wp-config.php before WordPress loads wp-settings.php.

// Trust the forwarded protocol only when the request comes from Server A.
$trusted_proxy_ips = [
    'A_SERVER_IP',
];

if (
    in_array($_SERVER['REMOTE_ADDR'] ?? '', $trusted_proxy_ips, true)
    && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
) {
    $forwarded_proto = strtolower(
        trim(explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO'])[0])
    );

    if ($forwarded_proto === 'https') {
        $_SERVER['HTTPS'] = 'on';
        $_SERVER['SERVER_PORT'] = 443;
    }
}

Replace A_SERVER_IP with the actual address of Server A.

The code should be placed before this line:

require_once ABSPATH . 'wp-settings.php';

Do Not Rewrite REMOTE_ADDR in WordPress

Some configurations use PHP code such as:

$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'];

or:

$_SERVER['REMOTE_ADDR'] = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];

This is not recommended when Nginx is already configured with the Real IP module.

Nginx should validate the trusted proxy and restore the real client IP before PHP receives the request.

After the backend Nginx configuration is correct, this PHP variable should already contain the visitor IP:

$_SERVER['REMOTE_ADDR']

Directly trusting X-Real-IP or X-Forwarded-For inside PHP can create an IP spoofing vulnerability when Server B is reachable without passing through Server A.

Step 5: Configure the Public WordPress URLs

The WordPress Address and Site Address should both use the public HTTPS URL.

In the WordPress dashboard, open:

Settings → General

Set the following values:

WordPress Address (URL): https://example.com/news
Site Address (URL):      https://example.com/news

Define the URLs in wp-config.php

You may explicitly define the public URLs in wp-config.php:

define('WP_HOME', 'https://example.com/news');
define('WP_SITEURL', 'https://example.com/news');

This prevents the URLs from being changed through the WordPress dashboard and ensures that WordPress consistently generates the correct public address.

Do not add a trailing slash to these values.

Update the URLs with WP-CLI

cd /var/www/example.com/news

wp option update home 'https://example.com/news'
wp option update siteurl 'https://example.com/news'

Update the URLs with SQL

UPDATE wp_options
SET option_value = 'https://example.com/news'
WHERE option_name IN ('home', 'siteurl');

The database table prefix may not be wp_, so verify the actual options table name before running the query.

Step 6: Restrict Direct Access to Server B

Whenever possible, Server B should accept web traffic only from Server A.

This can be enforced with:

  • A private network
  • A cloud security group
  • nftables
  • iptables
  • UFW or another host firewall
  • Nginx access-control directives

For example, inside the backend server block:

allow A_SERVER_IP;
deny all;

Be careful to allow any monitoring services, health checks, deployment systems, or trusted administration addresses that also require access.

A firewall rule is generally stronger than an Nginx access rule because it blocks unwanted traffic before it reaches the web server.

Step 7: Test the Reverse Proxy

Test the Public WordPress URL

curl -I https://example.com/news/

A successful response may resemble:

HTTP/2 200
content-type: text/html; charset=UTF-8

Test the Missing Trailing Slash

curl -I https://example.com/news

The expected response is:

HTTP/2 301
location: /news/

Check for Redirect Loops

curl -IL --max-redirs 10 https://example.com/news/

The request should eventually return a successful response instead of repeatedly redirecting between HTTP and HTTPS or between different hostnames.

Inspect Generated WordPress URLs

curl -s https://example.com/news/ | grep -Eo 'https?://[^"]+' | head

Generated URLs should begin with:

https://example.com/news/

They should not expose:

  • The backend IP address
  • The backend hostname
  • Plain HTTP URLs
  • URLs missing the /news prefix

Verify the Real Client IP

On Server B, monitor the Nginx access log:

sudo tail -f /var/log/nginx/access.log

Access the website from another device or network. The backend log should show the real visitor IP rather than the address of Server A.

Create a Temporary PHP Diagnostic File

<?php

header('Content-Type: text/plain');

echo 'REMOTE_ADDR: ';
echo $_SERVER['REMOTE_ADDR'] ?? 'not set';

echo PHP_EOL;

echo 'X-Forwarded-For: ';
echo $_SERVER['HTTP_X_FORWARDED_FOR'] ?? 'not set';

echo PHP_EOL;

echo 'X-Forwarded-Proto: ';
echo $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'not set';

echo PHP_EOL;

echo 'HTTPS: ';
echo $_SERVER['HTTPS'] ?? 'not set';

Remove the diagnostic file immediately after testing because it exposes request and infrastructure information.

Step 8: Troubleshoot Common Problems

Problem: Too Many Redirects

Common causes include:

  • WordPress does not recognize X-Forwarded-Proto
  • WP_HOME or WP_SITEURL uses HTTP
  • Server A and Server B both apply conflicting HTTPS redirects
  • A WordPress plugin forces a different hostname
  • A CDN or another proxy sends an unexpected forwarded protocol chain

Verify that Server A sends the following headers:

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;

Then confirm that wp-config.php changes HTTPS to on only when the request comes from the trusted proxy.

Problem: WordPress Redirects to the Backend IP

wp option get home
wp option get siteurl

Both values should be:

https://example.com/news

Also verify that Server A preserves the public hostname:

proxy_set_header Host $host;

Problem: CSS, JavaScript, or Images Return 404

Confirm that the WordPress URLs include /news and that the backend files are located under:

/var/www/example.com/news/

Also verify that Server A preserves the original URI:

location /news/ {
    proxy_pass http://wordpress_backend;
}

Do not unintentionally strip the /news/ prefix from the upstream request.

Problem: Backend Logs Show Server A’s IP

Confirm that Server B contains:

set_real_ip_from A_SERVER_IP;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Then verify that Server A sends:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Reload Nginx on both servers after changing the configuration.

Problem: WordPress Login Does Not Persist

This is usually caused by inconsistent HTTPS detection, mismatched domains, or incorrect cookie paths.

Confirm that:

  • The public URL always uses HTTPS
  • The website does not alternate between www and non-www hostnames
  • home and siteurl include /news
  • WordPress detects HTTPS before it initializes
  • No plugin forces a conflicting login URL

Optional: Add Nginx Proxy Caching

Nginx proxy caching can improve performance, but WordPress pages must not be cached indiscriminately.

Logged-in users, administrators, previews, POST requests, WooCommerce sessions, and personalized pages should bypass the cache.

Define the Cache Zone

Add the following directive to the http block on Server A:

proxy_cache_path /var/cache/nginx/wordpress
    levels=1:2
    keys_zone=wordpress_cache:100m
    max_size=10g
    inactive=60m
    use_temp_path=off;

Create Cache Bypass Variables

map $request_method $skip_cache_method {
    default 1;
    GET  0;
    HEAD 0;
}

map $http_cookie $skip_cache_cookie {
    default 0;
    ~*wordpress_logged_in 1;
    ~*wp-postpass 1;
    ~*comment_author 1;
    ~*woocommerce_items_in_cart 1;
    ~*woocommerce_cart_hash 1;
}

Enable Caching for /news/

location /news/ {
    proxy_pass http://wordpress_backend;

    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Connection "";

    set $skip_cache 0;

    if ($skip_cache_method) {
        set $skip_cache 1;
    }

    if ($skip_cache_cookie) {
        set $skip_cache 1;
    }

    if ($request_uri ~* "^/news/(wp-admin|wp-login\.php|wp-cron\.php|wp-json/)") {
        set $skip_cache 1;
    }

    proxy_cache wordpress_cache;
    proxy_cache_key "$scheme$request_method$host$request_uri";

    proxy_cache_bypass $skip_cache;
    proxy_no_cache $skip_cache;

    proxy_cache_valid 200 301 302 10m;
    proxy_cache_valid 404 1m;

    add_header X-Proxy-Cache $upstream_cache_status always;
}

This is only a starting point. Websites using WooCommerce, membership systems, multilingual plugins, dynamic personalization, or authentication integrations require additional cache exclusions.

Production Configuration Checklist

  • https://example.com/news/ loads successfully
  • /news redirects once to /news/
  • Server A preserves the original request URI
  • The original Host header reaches Server B
  • WordPress detects the forwarded HTTPS protocol
  • home and siteurl use the public HTTPS URL
  • Server B trusts forwarded IP headers only from Server A
  • Direct backend access is restricted where possible
  • WordPress login and administration pages are not cached
  • Backend IP addresses do not appear in redirects or page source
  • Nginx logs on Server B contain the real visitor IP address

Conclusion

A reliable WordPress reverse-proxy setup depends on consistent handling across every layer.

Server A must preserve the public hostname, request path, protocol, and client IP chain. Server B must trust only known proxies and use the Nginx Real IP module to restore the visitor address. WordPress must recognize the forwarded HTTPS protocol and generate URLs containing the correct /news prefix.

With these elements configured correctly, WordPress can operate securely and predictably behind Nginx while remaining publicly accessible at:

https://example.com/news/