
Putting Nginx in front of Apache (including a cPanel server) is a practical way to reduce CPU spikes from static files. It also helps you absorb short traffic bursts on a hosting VPS. This VPS reverse proxy setup guide tutorial walks through a production-style setup for 2026: accurate client IP logging, sane SSL termination, Let’s Encrypt/AutoSSL-friendly validation, and a rollback you can execute fast.
The model is simple. Nginx becomes the public entry point on ports 80/443. It then proxies to Apache on a private port.
You get faster static delivery, easier rate limiting, and a clean place to set security headers. You don’t have to rebuild your web stack.
What you’re building (and when it’s the right move)
This tutorial fits three common hosting setups:
- Ubuntu/Debian VPS running Apache (classic LAMP/LEMP hybrids) that needs better static performance and safer request buffering.
- cPanel/WHM servers where you want Nginx as a reverse proxy while keeping Apache handling dynamic PHP and existing vhosts.
- Multiple sites on one VPS where you want one front door (Nginx) and predictable backend behavior (Apache).
Skip this if you already run LiteSpeed. Also skip it if the real bottleneck is your database or PHP configuration.
If your load average jumps during asset-heavy traffic (images, CSS, JS, downloads), Nginx-in-front usually helps right away.
You’ll also need enough headroom to run two web servers (plus anything else on the box). A 1 vCPU VPS can work, but it’s easy to hit limits at peak.
For production hosting, consider a HostMyCode VPS.
If you want help with patching and ops, use managed VPS hosting.
Prerequisites checklist (do this before you touch ports)
- Root or sudo access and console access (cloud console / IPMI / rescue) in case you lock yourself out.
- One public IPv4 and a domain pointing to it.
- Working Apache vhosts already serving your sites on port 80 or 443.
- Firewall access (UFW/iptables/CSF). You’ll change exposed ports.
- Backups or at minimum a snapshot. Don’t skip this.
Before you change anything, capture a baseline. It makes comparison and recovery faster:
hostnamectl
nginx -v 2>/dev/null || true
apachectl -v 2>/dev/null || httpd -v
ss -lntp | egrep ':(80|443)\b'
apachectl -S 2>/dev/null || httpd -S
Also confirm disk space and file descriptors aren’t already tight:
df -h /
ulimit -n
Step 1: Install Nginx and enable a safe baseline config
On Ubuntu 24.04/24.10 and Debian 12/13, the distro Nginx package is fine for reverse proxy work. Install it:
sudo apt update
sudo apt install -y nginx
Keep your global config conservative at first. Edit /etc/nginx/nginx.conf and stick to sensible defaults.
Save “tuning” blocks for later. Do that after you measure:
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Test and start the service:
sudo nginx -t
sudo systemctl enable --now nginx
Step 2: Move Apache off public ports (bind to a backend port)
The clean pattern is simple. Nginx listens on 80/443. Apache listens on 127.0.0.1:8080.
You can also use 127.0.0.1:8443 if you proxy HTTPS internally.
Option A (recommended): proxy HTTP to Apache. Nginx terminates TLS, then proxies to Apache over HTTP on localhost. Your certs and TLS settings live in one place.
Option B: proxy HTTPS to Apache. This helps if you must keep Apache’s TLS handling, or a control panel expects it. It works, but it adds moving parts.
Ubuntu/Debian Apache: change Listen ports
Edit /etc/apache2/ports.conf:
Listen 127.0.0.1:8080
If you currently have Listen 80, remove or comment it.
Then update any vhost files referencing *:80 to 127.0.0.1:8080 or *:8080. Binding to localhost is safer.
Example vhost change in /etc/apache2/sites-available/example.conf:
<VirtualHost 127.0.0.1:8080>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example
# If behind proxy, allow forwarded protocol
SetEnvIf X-Forwarded-Proto https HTTPS=on
<Directory /var/www/example>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/example_error.log
CustomLog ${APACHE_LOG_DIR}/example_access.log combined
</VirtualHost>
Enable proxy-aware modules. These are commonly needed for real IP and scheme handling:
sudo a2enmod remoteip headers rewrite
Restart Apache and confirm it’s listening where you expect:
sudo systemctl restart apache2
ss -lntp | egrep ':(8080)\b'
cPanel note (WHM/Apache)
On cPanel servers, avoid hand-editing Apache ports and vhosts. WHM regenerates configs and will overwrite your changes.
Use an Nginx reverse proxy integration (or a cPanel-supported plugin) instead.
The rest of this guide still matters. In particular, keep real IP handling and AutoSSL behavior correct.
Apply the changes in a way WHM won’t undo.
If you’re also moving accounts between WHM servers as part of the redesign, keep this reference nearby: cPanel account transfer troubleshooting.
Step 3: Configure Nginx reverse proxy server blocks (HTTP + HTTPS)
Create a new site file: /etc/nginx/sites-available/example.com:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream apache_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# ACME challenge path for Let's Encrypt (if you use certbot --webroot)
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type "text/plain";
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Sensible TLS for 2026; keep it simple and compatible
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# Uploads and timeouts: tune to your site, not guesswork
client_max_body_size 64m;
proxy_read_timeout 120;
# Pass correct client and scheme details to Apache
location / {
proxy_http_version 1.1;
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;
# WebSockets (if your app uses them)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://apache_backend;
}
# Optional: serve static assets directly from Nginx for less Apache work
location ~* \.(css|js|jpg|jpeg|png|gif|svg|ico|webp|avif|woff2?)$ {
root /var/www/example;
access_log off;
expires 30d;
add_header Cache-Control "public, max-age=2592000";
try_files $uri @proxy_to_apache;
}
location @proxy_to_apache {
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_pass http://apache_backend;
}
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx
Common pitfall: if you serve static files from Nginx, the root path must match your real document root.
On WordPress, make sure Nginx can read files under wp-content/uploads.
Step 4: Make Apache log real client IPs (not 127.0.0.1)
If you skip this, every request will look like it came from localhost. That breaks analytics, rate limiting, fraud checks, and incident response timelines.
Apache can trust Nginx’s forwarded header via mod_remoteip. Create or edit /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
Then confirm logs show your public IP:
curl -sI https://example.com | head
sudo tail -n 5 /var/log/apache2/example_access.log
If you still see 127.0.0.1, check these items:
- Nginx is sending
X-Forwarded-For - Apache has
mod_remoteipenabled - You’re trusting the right proxy IP (localhost)
Step 5: Handle Let’s Encrypt without breaking renewals
You have two clean paths here:
- Terminate TLS in Nginx (recommended): Nginx owns certificates, and you renew on the host using
certbot. - Keep AutoSSL/cPanel cert flow: If a control panel manages certs, HTTP-01 validation still has to reach port 80 cleanly.
Certbot on Nginx (webroot method)
Create the webroot referenced by the Nginx config:
sudo mkdir -p /var/www/letsencrypt
sudo chown -R www-data:www-data /var/www/letsencrypt
Install certbot and request a cert:
sudo apt install -y certbot
sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.com -d www.example.com
Reload Nginx after renewals by adding a deploy hook:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
If ACME challenges start failing, this guide helps you narrow it down quickly: TLS certificate renewal troubleshooting.
Step 6: Fix “HTTPS redirect loops” and mixed-content issues
After Nginx terminates TLS, Apache only sees HTTP unless you pass the original scheme. You already set X-Forwarded-Proto in Nginx.
Now your app has to respect it.
For WordPress behind a TLS-terminating proxy, add this to wp-config.php (only if needed):
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
Also confirm WordPress Address and Site Address use https://.
Most redirect loops happen when two components try to “fix” the URL. The usual triggers are a hostname mismatch, a scheme mismatch, or both.
If you want to add hardened headers at the proxy layer, keep them compatible with WordPress and panel apps. This walkthrough sticks to safe header sets: Nginx security headers configuration.
Step 7: Update firewall rules without locking yourself out
Only expose what must be public. With Nginx on 80/443 and Apache on 8080 (localhost), don’t open 8080 to the internet.
With UFW, a basic web rule set looks like:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 8080/tcp
sudo ufw status verbose
If you want a hosting-oriented baseline that won’t break SSH/DNS/web traffic, follow: UFW firewall setup without breaking SSH/DNS/web.
Step 8: Quick performance wins (without “magic tuning”)
Reverse proxy setups often slow down because buffering or caching was applied blindly. Start with a few small changes you can measure. Each one is easy to roll back.
1) Enable proxy buffering (good default)
Add inside the location / block (or server-level) if Apache workers get stuck serving slow clients:
proxy_buffering on;
proxy_buffers 16 64k;
proxy_busy_buffers_size 128k;
2) Serve static files directly from Nginx (verify paths)
You already have a static location block example above. Confirm permissions before you assume it’s “working”:
sudo -u www-data test -r /var/www/example/index.php && echo OK
3) Don’t cache HTML until you understand your app
Full-page caching (Varnish/FastCGI cache) can help a lot. It can also cache logged-in pages by mistake.
If you’re ready for that layer, use a dedicated guide like: Varnish cache setup.
Step 9: Validate the setup (tests you can run in 10 minutes)
- Ports:
ss -lntp | egrep ':(80|443|8080)\b'(Apache should not listen on 0.0.0.0:80 anymore). - HTTP to HTTPS:
curl -I http://example.comshould return 301 to https. - TLS chain:
openssl s_client -connect example.com:443 -servername example.com -showcerts - Real IP: Apache access logs should show your client IP.
- Backend health:
curl -I http://127.0.0.1:8080should return a valid response.
Keep both error logs open while you test. You’ll spot failures immediately:
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/apache2/error.log
Rollback plan (print this before you start)
If something goes sideways on a production server, you want a short, repeatable exit. This rollback puts Apache back on port 80 quickly.
- Disable the Nginx site:
sudo rm /etc/nginx/sites-enabled/example.com - Stop Nginx:
sudo systemctl stop nginx - Restore Apache
Listen 80in/etc/apache2/ports.confand vhosts - Restart Apache:
sudo systemctl restart apache2 - Confirm:
curl -I http://example.com
Keep a root console open during the cutover. If you’re also changing firewall rules, apply those last.
Operational checklist for hosting environments
- Backups: confirm you can restore, not just create archives. This tutorial helps: VPS backup restore plan.
- Retention: don’t keep daily backups forever. Use a policy: backup retention policy.
- Monitoring: alert on 5xx spikes, disk usage, and certificate expiry.
- Email: if the same VPS also sends mail, keep PTR/rDNS correct to avoid deliverability problems. Start here: reverse DNS setup.
Summary: a clean Nginx front door with Apache compatibility
You now have Nginx on the public edge (80/443) and Apache running safely on a backend port. You also have real client IPs in Apache logs, plus a certificate flow that won’t surprise you later.
In practice, this usually means fewer Apache workers tied up on slow clients and steadier response times during bursts. Nginx buffers requests and serves static assets efficiently.
If you want this running on reliable infrastructure with predictable network performance, start on a HostMyCode VPS.
If you don’t want to spend your week on updates, firewall tweaks, and incident response, managed VPS hosting is often the sensible choice for production sites.
If you’re planning a proxy cutover for a business site, give yourself enough CPU and RAM to run Nginx + Apache comfortably. HostMyCode VPS plans work well for multi-site hosting, and managed VPS hosting can cover patching and troubleshooting so you’re not on-call for every issue.
FAQ
Will this break cPanel AutoSSL?
It can, if HTTP-01 validation no longer reaches the expected webroot on port 80. Ensure Nginx serves /.well-known/acme-challenge/ correctly, or use a cPanel-supported Nginx integration that preserves AutoSSL’s validation path.
Should I proxy HTTPS to Apache instead of terminating TLS in Nginx?
Terminate TLS in Nginx unless you have a specific requirement to keep Apache’s TLS behavior. TLS termination at Nginx simplifies cert renewals and makes header/security policy consistent.
Why do my logs show 127.0.0.1 for every request?
Apache is logging the proxy IP. Enable mod_remoteip, set RemoteIPHeader X-Forwarded-For, and trust 127.0.0.1 (or your proxy IP) as a trusted proxy.
How do I prevent people from hitting Apache directly?
Bind Apache to 127.0.0.1 (or a private interface) and block the backend port in your firewall. Don’t let Apache listen on 0.0.0.0:8080 unless you have a private network in front.
What’s the safest way to test the cutover?
Lower DNS TTL ahead of time, test the Nginx config with nginx -t, keep a console session open, and validate from an external network with curl -I and browser checks before you declare it done.