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
Hostheader is preserved - WordPress
homeandsiteurluse 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
nftablesiptables- 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
/newsprefix
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_HOMEorWP_SITEURLuses 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
wwwand non-wwwhostnames homeandsiteurlinclude/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/newsredirects once to/news/- Server A preserves the original request URI
- The original
Hostheader reaches Server B - WordPress detects the forwarded HTTPS protocol
homeandsiteurluse 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/