nginx Reverse Proxy in Front of Apache
Run Apache on 127.0.0.1:8080 and let nginx own ports 80 and 443, forwarding requests with proxy_pass and the Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto headers. mod_remoteip then restores the visitor's real IP in Apache logs, and nginx serves static files without touching PHP.
A verified configuration walkthrough for putting nginx in front of Apache on a Linux VPS, with the redirect-loop traps called out.
knowledge
- Read time
- 4 min
- Sections
- 7
- Updated
- 14 Nov 2026
- Written by
- WebJi® team
Last editorial update: 15 November 2026.
Why put nginx in front of Apache at all?
The point is division of labour, not replacement: nginx terminates TLS, absorbs slow clients and serves static files cheaply, while Apache keeps the .htaccess rules, mod_rewrite and PHP configuration your site already depends on. Apache is an excellent application server; what it handles less gracefully is thousands of keep-alive connections and small static files. nginx in front takes both loads off it.
- nginx: low memory per connection, event-driven, strong at TLS, static files and buffering slow clients.
- Apache: keeps .htaccess, per-directory rewrites and the module stack the application expects, unchanged.
- Static offload: images, CSS and JS can be served by nginx straight from disk, never reaching PHP.
- Room to grow: one nginx instance can front several backends on the same box.
The cost is one more moving part: two services to restart, two logs to read, and a class of bug where the application believes it is on plain HTTP while the browser is on HTTPS. Both traps are covered below. If your real question is which single webserver to run instead, the nginx vs LiteSpeed comparison and the LiteSpeed vs Apache guide handle that decision.
Which service listens on which port?
Only nginx accepts traffic from the internet on ports 80 and 443; Apache retreats to loopback port 8080, so nothing reaches it except through the proxy. Keeping 8080 firewalled from outside is part of the design — an internet-reachable Apache behind the proxy would let visitors bypass nginx entirely.
| Layer | Listens on | Job |
|---|---|---|
| nginx | 0.0.0.0:80 and :443 | TLS, static files, buffering, proxying |
| Apache | 127.0.0.1:8080 | PHP, .htaccess, application logic |
| Firewall | 22, 80, 443 open | 8080 closed to the outside |
Before changing anything, see what already owns the ports with ss -ltnp. Two services fighting over port 80 is the most common first failure of this migration.
Step 1 — move Apache to loopback before nginx exists
Change Apache first, so there is never a moment when both daemons want port 80. On Ubuntu and Debian the listen directive lives in /etc/apache2/ports.conf; on RHEL-family systems edit /etc/httpd/conf/httpd.conf instead.
# /etc/apache2/ports.conf
Listen 127.0.0.1:8080
Every VirtualHost that names a port must follow:
<VirtualHost 127.0.0.1:8080>
ServerName example.com
DocumentRoot /var/www/example
AllowOverride All
</VirtualHost>
Restart and verify: sudo systemctl restart apache2 (or httpd), then curl -I http://127.0.0.1:8080 and ss -ltnp | grep 8080. It worked when Apache answers on loopback and nothing answers on port 80. Keep AllowOverride All — without it .htaccess files are ignored even though they are present, which surfaces later as broken permalinks.
Step 2 — the nginx server block with proxy_pass
One server block per site, whose location / hands every dynamic request to Apache with proxy_pass http://127.0.0.1:8080 and the four forwarding headers. The headers are the part people omit, and each has a job: Host preserves the domain, X-Real-IP carries the visitor address, X-Forwarded-For builds the chain, X-Forwarded-Proto tells Apache the original scheme.
# /etc/nginx/sites-available/example.com.conf
server {
listen 80;
server_name example.com;
# static files served by nginx, never reaching PHP
location ~* \.(css|js|jpg|jpeg|png|webp|svg|gif|ico|woff2)$ {
root /var/www/example;
expires 30d;
access_log off;
}
location / {
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 $scheme;
proxy_read_timeout 60s;
}
}
This is the header pattern from nginx's own proxy-module documentation: Host and X-Real-IP appear in its example block, and $proxy_add_x_forwarded_for is the documented variable that appends the client address to any incoming X-Forwarded-For chain. Symlink the file into sites-enabled, then sudo nginx -t — a syntax error stops the reload, which is the safe failure — and apply with sudo systemctl reload nginx. Add TLS afterwards with certbot --nginx so the certificate lands on the proxy, where TLS now terminates. Leave proxy_read_timeout at 60 seconds unless a known report needs longer; a too-short timeout turns a slow page into a 504 that looks like an application bug.
Step 3 — make Apache and WordPress see the real IP and HTTPS
Behind the proxy Apache sees a plain HTTP request from 127.0.0.1, so you must restore the visitor's address with mod_remoteip and the original scheme with the forwarded proto header. Without this, every log line, rate limit and WordPress HTTPS redirect behaves as if the whole internet were one loopback client.
In Apache, enable the module and declare which header carries the client address, per the official mod_remoteip documentation:
sudo a2enmod remoteip
# then in the VirtualHost or a conf file:
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1
RemoteIPTrustedProxy 127.0.0.1 matters more than it looks: the Apache docs warn that without it mod_remoteip trusts any host that presents the header, which would let a client spoof its IP.
On the WordPress side, teach it that a forwarded HTTPS request is HTTPS, in wp-config.php above the "stop editing" line:
if ( isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) {
$_SERVER['HTTPS'] = 'on';
}
The classic failure is a redirect loop: a force-HTTPS plugin redirects to https, Apache reports HTTP because the proto header never arrived, and the browser is bounced forever. If the loop starts, disable the force-HTTPS rule until curl -I proves the X-Forwarded-Proto header is reaching Apache.
Verify the proxy, then read the logs in the right order
Three checks prove the stack: nginx answers on the domain, Apache answers only on loopback, and the Apache log shows the visitor's real IP. Do them in this order after every change.
curl -I https://example.com— the Server header should say nginx; that is the proxy answering.curl -I http://127.0.0.1:8080— Apache must answer here, and only here.- Load a page and read the Apache access log: if entries show 127.0.0.1 as the client, mod_remoteip is not active.
- Open a permalink that depends on .htaccess. A 404 means AllowOverride is off in the proxied VirtualHost.
When something breaks, read nginx's error log first and Apache's second: an upstream timeout in nginx tells you to check whether the request reached Apache at all. For deeper WordPress-side debugging, the error-log guide covers where each log lives; the 500-error walkthrough handles the case where the application itself is faulting.
Where this layout belongs on WebJi®
Running two daemons with root access needs a VPS, not a shared plan: WebJi® VPS plans start from ₹499 a month with a configuration scoped and quoted per order, on KVM with NVMe storage. Shared plans at WebJi® already put a webserver with built-in caching in front of PHP, so a hand-built proxy stack there is solving a problem you do not have.
If you would rather speed up WordPress on the hosting you already pay for, Redis object caching and the broader WordPress speed guide deliver most of the win without a second daemon. For a proxy that will not settle, email support@webji.services with the nginx -t output and the two relevant log lines — support is 24/7 in Hindi and English, published average first reply under 60 minutes.
Frequently asked questions
How do I set up nginx as a reverse proxy for Apache?
Move Apache to 127.0.0.1:8080 in ports.conf and its VirtualHosts, then create an nginx server block on port 80 whose location / uses proxy_pass http://127.0.0.1:8080 with the Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto headers. Run nginx -t, reload nginx, then issue the TLS certificate with certbot on the proxy.
Does .htaccess still work behind an nginx proxy?
Yes. Apache still processes the request; nginx only forwards it. The requirement is AllowOverride enabled in the proxied VirtualHost — with it off, the rules are silently ignored even though the file exists, which typically shows up as broken permalinks.
What does proxy_pass actually do?
It tells nginx to forward matching requests to another server instead of serving them from disk. With proxy_pass http://127.0.0.1:8080, every request in that location is handed to Apache on loopback and the response is passed back to the client. It accepts an HTTP address, an HTTPS address, a Unix socket or a named upstream group.
How do I show the real visitor IP in Apache logs?
Send X-Forwarded-For from nginx, then run a2enmod remoteip and set RemoteIPHeader X-Forwarded-For with RemoteIPTrustedProxy 127.0.0.1 in Apache. Without mod_remoteip every visitor appears as 127.0.0.1 in logs and rate limits; without the trusted-proxy line, Apache's docs warn the header would be trusted from any host.
Which server should handle PHP in this setup?
Apache. nginx only proxies and serves static files; PHP stays behind Apache so .htaccess, mod_rewrite and the application's existing module configuration keep working unchanged. The alternative — nginx with PHP-FPM and no Apache at all — is a different architecture, not a variation of this one.
Last editorial update: 15 November 2026. Prices follow the dated sources on this page; checkout confirms current charges.
Ready to get started with WebJi®?
Join 12,000+ businesses across India and overseas. INR pricing, 18% GST shown before payment and 24/7 email support in Hindi and English — average first reply under 60 minutes.