Why put nginx in front of Apache at all
Run Apache on 127.0.0.1:8080, let nginx own ports 80 and 443, and forward requests to Apache with proxy_pass. Send X-Forwarded-For and X-Forwarded-Proto so Apache and WordPress see the real visitor and HTTPS, then reload both services and test the headers.
The point is not to replace Apache. It is to let each do the job it is better at: nginx terminates TLS and serves static files quickly, while Apache keeps the .htaccess rules and PHP setup a site already depends on.
- nginx handles slow clients, keep-alive and TLS with a small memory footprint.
- Apache keeps mod_rewrite, .htaccess, and any module the application expects.
- Static files can be served by nginx directly, never reaching PHP.
- One nginx instance can front several Apache backends on the same box.
The cost is one more moving part. Two services to restart, two sets of logs to read, and a class of bug where the application thinks it is on HTTP while the browser is on HTTPS.
Architecture: who listens on which port
Only nginx accepts traffic from the internet. Apache listens on loopback, so nothing reaches it except through the proxy.
| Layer | Listens on | Job |
|---|---|---|
| nginx | 0.0.0.0:80 and :443 | TLS, static files, caching, proxying |
| Apache | 127.0.0.1:8080 | PHP, .htaccess, application logic |
| Firewall | 22, 80, 443 open | 8080 must not be reachable from outside |
Check what is already listening with ss -ltnp before you change anything. Two services fighting over port 80 is the most common first failure.
Move Apache to loopback first
Change Apache before nginx, so there is never a moment when both want port 80.
- On Ubuntu or Debian, edit /etc/apache2/ports.conf and change Listen 80 to Listen 127.0.0.1:8080. On RHEL-family systems the same line lives in /etc/httpd/conf/httpd.conf.
- Update any VirtualHost that names a port, so it reads <VirtualHost 127.0.0.1:8080>.
- Restart Apache: sudo systemctl restart apache2 (or httpd).
- Confirm with curl -I http://127.0.0.1:8080 and with ss -ltnp | grep 8080. You know it worked when Apache answers on loopback and nothing answers on port 80.
The nginx server block
One server block per site, in /etc/nginx/sites-available/, symlinked into sites-enabled. The proxy headers are the part people omit.
- Create /etc/nginx/sites-available/example.com.conf with a server block on port 80 that sets server_name to the domain.
- Add a location / block containing proxy_pass http://127.0.0.1:8080; and the four headers: Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto.
- Optionally add location ~* \.(css|js|jpg|png|webp|svg|woff2)$ with a root and expires 30d, so nginx serves static files itself.
- Symlink it into sites-enabled, then run sudo nginx -t. A syntax error here stops the reload, which is the safe failure.
- Reload with sudo systemctl reload nginx, and add TLS afterwards with certbot --nginx so the certificate lands on the proxy, not on Apache.
Keep proxy_read_timeout at 60s unless you know a request needs longer. A short timeout turns a slow report into a 504 that looks like an application bug.
Make Apache and WordPress believe the visitor is on HTTPS
Behind the proxy, Apache sees a plain HTTP request from 127.0.0.1. Without the forwarded headers, WordPress builds http:// links on an https:// page and the browser blocks them as mixed content.
- Set X-Forwarded-Proto $scheme in nginx, so Apache can tell the original request was HTTPS.
- In WordPress, add a check on HTTP_X_FORWARDED_PROTO in wp-config.php, or let the host's proxy configuration set HTTPS=on.
- Map the real client IP with mod_remoteip in Apache, so logs and rate limits see the visitor, not 127.0.0.1.
The failure to watch for is a redirect loop: WordPress forces HTTPS, Apache reports HTTP, the browser is sent back and forth. If the loop starts, disable the force-HTTPS plugin or rule until the forwarded header is correct.
Verify it, then read the logs in the right order
Two commands tell you which service answered and what it believed about the request.
- curl -I https://example.com and read the Server header. If it says nginx, the proxy is answering.
- curl -I http://127.0.0.1:8080 and confirm Apache answers directly on loopback.
- Load a page and check the nginx access log for the real client IP; if it shows 127.0.0.1 in the Apache log, mod_remoteip is not configured.
- Request a URL that depends on .htaccess, such as a permalink. A 404 here means the rewrite did not survive the proxy, usually because AllowOverride is off in the proxied VirtualHost.
Read nginx's error log first and Apache's second. When nginx reports an upstream timeout, the Apache log tells you whether the request arrived at all.
Where this belongs on WebJi®
This layout needs root and two services, so it belongs on a VPS rather than a shared plan. WebJi® VPS plans give root access on KVM with NVMe storage, and the nginx and LiteSpeed comparison covers the case where you would rather replace Apache than proxy to it.
Email support@webji.services with the nginx -t output and the two log lines if the proxy will not settle. Support is 24/7 by email in Hindi and English, average first reply under 60 minutes, and the office phone is not a support line.
Frequently Asked Questions
How do I set up nginx as a reverse proxy for Apache?
Move Apache to 127.0.0.1:8080, then create an nginx server block on port 80 whose location / uses proxy_pass http://127.0.0.1:8080 with Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto headers. Run nginx -t, reload nginx, then add TLS with certbot on the proxy.
Why put nginx in front of Apache?
nginx handles TLS and static files with less memory per connection, while Apache keeps the .htaccess rules and modules the site already relies on. The combination is common on servers running several sites with years of rewrite rules.
Does .htaccess still work behind an nginx proxy?
Yes, because Apache still processes the request. nginx only forwards it. AllowOverride must stay enabled in the Apache VirtualHost, or the rules are ignored even though the file is present.
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 and the response is returned to the client unchanged.
How do I pass the real visitor IP to Apache?
Send X-Forwarded-For and X-Real-IP from nginx, then enable mod_remoteip in Apache and set RemoteIPHeader X-Forwarded-For. Without it, every visitor appears as 127.0.0.1 in the Apache log and in any rate limiting.
Which server should handle PHP requests?
Apache, in this layout. nginx only proxies and serves static files; PHP-FPM stays behind Apache so .htaccess, mod_rewrite and the application's existing configuration keep working exactly as before.

