Back to tutorials
Tutorial

Reverse Proxy Setup Tutorial (2026): Put Nginx in Front of Apache on a VPS for Safer SSL and Faster Static Files

Reverse proxy setup tutorial for 2026: configure Nginx + Apache on a VPS for SSL termination, real IPs, and safer headers.

By Anurag Singh
Updated on Aug 29, 2026
Category: Tutorial
Share article
Reverse Proxy Setup Tutorial (2026): Put Nginx in Front of Apache on a VPS for Safer SSL and Faster Static Files

Running Apache and Nginx together isn’t overkill. On a busy VPS, it’s often the simplest way to keep legacy .htaccess sites working. You can still move SSL, HTTP/2, and static file delivery to a faster front door.

This reverse proxy setup tutorial walks through a production-style “Nginx in front of Apache” layout on Ubuntu. It includes real visitor IPs, WebSocket support, and practical security defaults.

This guide is for agencies, resellers, and developers who need predictable migrations. If you host multiple WordPress or PHP sites, you may not be ready for pure Nginx. This pattern buys time while you modernize the edge.

What you’ll build (and when this pattern makes sense)

You’ll end up with:

  • Nginx on ports 80/443 as the public entry point (TLS termination, HTTP/2/HTTP/3 optional, rate limiting if you want it).
  • Apache on an internal port (for PHP via prefork+mod_php or event+PHP-FPM, plus .htaccess compatibility).
  • Correct client IP logging in Apache using X-Forwarded-For and Apache’s mod_remoteip.
  • Optional static file offload so Nginx serves assets directly and Apache handles dynamic requests.

This layout is a good fit if any of these apply:

  • You’re migrating sites that rely on .htaccess, per-directory rewrites, or Apache-only modules.
  • You want one place to enforce TLS settings, HSTS, and headers (Nginx), even if Apache vhosts vary.
  • You need better concurrency for static files and keep-alive connections than Apache alone typically delivers.

If you’re already happy on pure Nginx, don’t add moving parts. If you’re actively moving from Apache to Nginx, use our migration-focused walkthrough: Apache to Nginx migration tutorial.

Prerequisites and a quick preflight checklist

Assumptions:

  • Ubuntu Server 24.04 LTS or 22.04 LTS on a VPS
  • Root or sudo access
  • A domain name pointing to your server IP
  • Existing Apache site(s) you want to keep running

Before you change ports or configs, capture a quick snapshot of what’s working. It makes rollback and troubleshooting much easier.

  • Confirm Apache is healthy: curl -I http://127.0.0.1 (or your current site URL)
  • List enabled vhosts: ls -1 /etc/apache2/sites-enabled/
  • Check listening ports: ss -lntp | egrep ':(80|443)\b'

If you’d rather not own the OS-level details, managed VPS hosting from HostMyCode is a good fit for this “keep Apache, modernize the edge” setup.

Step 1: Install Nginx and required Apache modules

Install Nginx. Then enable Apache modules for forwarded IP handling and common proxy/header behavior:

sudo apt update
sudo apt install -y nginx apache2

# Enable modules for proxy awareness + headers
sudo a2enmod remoteip headers rewrite ssl proxy proxy_http
sudo systemctl enable --now nginx apache2

If Apache is already installed, that’s fine. The key is enabling remoteip and headers. Without them, logs and proxy behavior are usually wrong.

Step 2: Move Apache off ports 80/443 (bind it to an internal port)

Nginx and Apache can’t both bind to the same public ports. Split responsibilities cleanly:

  • Nginx listens on :80 and :443
  • Apache listens on 127.0.0.1:8080 (and optionally 127.0.0.1:8443 if you want internal TLS, which most stacks don’t need)

Edit Apache’s ports:

sudo nano /etc/apache2/ports.conf

Set it like this (example):

Listen 127.0.0.1:8080

<IfModule ssl_module>
    # Keep SSL disabled in Apache for this pattern, or bind internally if required.
    # Listen 127.0.0.1:8443
</IfModule>

Next, update your Apache vhost(s) to match the new bind address. For a typical site file in /etc/apache2/sites-available/example.conf:

<VirtualHost 127.0.0.1:8080>
    ServerName example.com
    ServerAlias www.example.com

    DocumentRoot /var/www/example.com/public

    <Directory /var/www/example.com/public>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/example-error.log
    CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>

If you currently have an Apache SSL vhost enabled, disable it. You’ll terminate TLS at Nginx instead.

sudo a2dissite default-ssl.conf 2>/dev/null || true
sudo apache2ctl configtest
sudo systemctl restart apache2

Verify Apache is now only reachable internally:

ss -lntp | egrep ':(80|443|8080)\b'
curl -I http://127.0.0.1:8080

Step 3: Create the Nginx reverse proxy server block

Create an Nginx site file:

sudo nano /etc/nginx/sites-available/example.com.conf

Use this as a baseline. It redirects HTTP→HTTPS, forwards the headers Apache expects, and supports WebSockets:

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

    server_name example.com www.example.com;

    # ACME challenge for Let's Encrypt
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        default_type "text/plain";
    }

    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 defaults for 2026 (keep this maintained)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    # Security headers (adjust CSP per app)
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Enable HSTS only after confirming HTTPS is stable
    # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Allow larger uploads if you host WordPress/media
    client_max_body_size 64m;

    # Proxy settings
    location / {
        proxy_pass http://127.0.0.1:8080;
        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;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_read_timeout 120s;
        proxy_connect_timeout 10s;
    }
}

Add the WebSocket helper map once (global) if it’s not already present. In /etc/nginx/nginx.conf, inside the http {} block:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

Enable the site and test:

sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 4: Get SSL certificates (Let’s Encrypt) without touching Apache

Once Nginx owns ports 80/443, issue certificates through Nginx:

sudo apt install -y certbot python3-certbot-nginx

# Create the webroot used above
sudo mkdir -p /var/www/letsencrypt
sudo chown -R www-data:www-data /var/www/letsencrypt

sudo certbot --nginx -d example.com -d www.example.com

If you prefer tighter control (common on multi-site servers), run Certbot with --webroot and keep Nginx edits manual. For a deeper SSL walkthrough, see: Let’s Encrypt setup guide.

Check renewals:

sudo systemctl status certbot.timer
sudo certbot renew --dry-run

Step 5: Fix Apache logs and application IP detection (mod_remoteip)

If you skip this, Apache will log 127.0.0.1 for every request. That breaks IP-based rules, analytics, and many WordPress security tools.

Create a small config file:

sudo nano /etc/apache2/conf-available/remoteip.conf

Use:

RemoteIPHeader X-Forwarded-For

# Trust only the local proxy (Nginx on the same server)
RemoteIPTrustedProxy 127.0.0.1
RemoteIPTrustedProxy ::1

Enable it and restart Apache:

sudo a2enconf remoteip
sudo systemctl restart apache2

Now update Apache’s log format so it records the real client IP. The simplest approach is switching from %h to %a in your vhost logs. That works because mod_remoteip updates the connection address.

# In your vhost file:
CustomLog ${APACHE_LOG_DIR}/example-access.log "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""

Quick test (from your workstation):

curl -I https://example.com
sudo tail -n 5 /var/log/apache2/example-access.log

Step 6: Offload static files to Nginx (optional, but worth it)

If Apache still serves everything, you’ll still get centralized TLS and headers. The performance win is usually smaller, though. Letting Nginx serve static assets (CSS/JS/images/fonts) reduces Apache worker load and often improves caching.

Two rules keep you out of trouble:

  • Only offload paths you know are real files and don’t require auth or PHP logic.
  • Don’t offload /wp-admin or anything behind Basic Auth.

Add this inside the server { ... } 443 block, above the proxy location /:

# Point Nginx to the same docroot Apache uses
root /var/www/example.com/public;

location ~* \.(?:css|js|jpg|jpeg|png|gif|svg|webp|ico|woff2?|ttf|eot)$ {
    access_log off;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, immutable";
    try_files $uri @apache;
}

location @apache {
    proxy_pass http://127.0.0.1:8080;
    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;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 120s;
}

Reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Quick diagnostic: check cache headers for a CSS file:

curl -I https://example.com/wp-content/themes/yourtheme/style.css

Step 7: Tighten timeouts and buffers to avoid common proxy failures

Two issues show up constantly on VPS hosting:

  • 504 Gateway Timeout during long PHP requests (imports, backups, WooCommerce reports).
  • Large request body errors on uploads.

Uploads are handled with client_max_body_size (already in the config). For timeouts, keep the settings deliberate:

  • Keep proxy_connect_timeout low (5–10s).
  • Raise proxy_read_timeout only when you can justify it.
  • Align PHP limits (max_execution_time, request_terminate_timeout in PHP-FPM if used) to avoid confusing partial failures.

If you run WordPress on a VPS, pair this with object caching and basic PHP-FPM tuning. The Redis setup here works with either Apache or Nginx: WordPress Redis object cache setup.

Step 8: Basic firewall rules so only Nginx is reachable from the internet

Apache is bound to 127.0.0.1, so it isn’t exposed directly. A firewall still helps. It reduces the chance you expose other services later.

With UFW:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

If you lock yourself out or ACME challenges stop working after firewall changes, this guide gets you back to stable quickly: UFW firewall troubleshooting tutorial.

Step 9: Validate the chain end-to-end (tests you can run in 5 minutes)

Run these after each major change. They catch most mistakes immediately.

  • Nginx config: sudo nginx -t
  • Apache config: sudo apache2ctl configtest
  • HTTP redirect: curl -I http://example.com should be 301 to HTTPS
  • HTTPS response: curl -I https://example.com should be 200/301 as expected
  • Origin reachability: curl -I http://127.0.0.1:8080 should return a valid response
  • Real IP in Apache logs: check access logs show your public IP, not 127.0.0.1

If you suspect the wrong host header is being forwarded (common on multi-site boxes), add a temporary debug header in Nginx:

add_header X-Debug-Host $host always;

Then confirm it’s what you expect:

curl -I https://example.com | grep -i x-debug-host

Common pitfalls (and the exact symptom you’ll see)

  • Infinite redirect loop: your app thinks it’s on HTTP because it doesn’t respect X-Forwarded-Proto. Fix by ensuring Nginx sets it, and your app/framework trusts proxies.
  • Mixed-content warnings: hard-coded HTTP URLs in WordPress or your theme. Update site URL and run a search/replace if needed.
  • Wrong visitor IP: you skipped mod_remoteip or you trusted too many proxies. Only trust 127.0.0.1 unless you have an upstream load balancer.
  • 502 Bad Gateway: Apache isn’t listening on 8080, or a local firewall blocks loopback (rare). Check ss -lntp and journalctl -u apache2 -n 50.
  • ACME challenge fails: the /.well-known/acme-challenge/ location is missing or wrong webroot permissions. Verify /var/www/letsencrypt is readable by Nginx.

Operational checklist for hosting teams (keep this with your runbooks)

  • Pin down ports: Nginx public :80/:443; Apache loopback :8080
  • Automate cert renewal checks: certbot renew --dry-run monthly
  • Enable real IP logging: Nginx headers + Apache mod_remoteip
  • Set consistent headers in Nginx (then remove duplicates in Apache if present)
  • Decide static offload rules per site; don’t guess
  • Log monitoring: alert on 5xx spikes and upstream errors

For practical alerting on authentication failures, web errors, and suspicious scans, use our VPS monitoring walk-through: VPS log monitoring tutorial.

Summary: a stable edge without rewriting your stack

This Nginx+Apache layout keeps Apache compatibility while giving you one place to manage TLS, headers, and edge behavior. It also makes migrations less stressful.

You can move Apache vhosts around while keeping a consistent Nginx front end across servers.

If you’re deploying this on production infrastructure, plan enough headroom for two web servers plus caching. A HostMyCode VPS is a solid fit for single-server deployments, and managed VPS hosting is the right call if you want help validating configs and hardening the edge.

If you’re standardizing multiple sites behind one Nginx edge, start with a VPS that delivers consistent CPU performance and NVMe I/O. HostMyCode offers VPS hosting for hands-on admins, and managed VPS hosting if you want the proxy, SSL, and monitoring pieces reviewed by an ops team.

FAQ

Should Apache run on 127.0.0.1:8080 or on a private network IP?

On a single VPS, 127.0.0.1:8080 is the simplest and safest option. Use a private IP only if Nginx and Apache run on different servers.

Do I need SSL enabled in Apache too?

Usually no. Terminate TLS at Nginx, proxy to Apache over loopback HTTP, and keep Apache’s SSL vhosts disabled to reduce moving parts.

How do I confirm WordPress sees the correct HTTPS scheme behind the proxy?

Make sure the WordPress Site Address is set to HTTPS. Then confirm Nginx sends X-Forwarded-Proto. If a plugin still reports HTTP, WordPress (or the plugin) likely isn’t trusting proxy headers.

Will this work with cPanel or Plesk?

This guide assumes a self-managed VPS. Control panels often manage ports and web server configs in their own way. If you use a panel, follow its supported “Nginx as reverse proxy” method to avoid conflicts.

What’s the safest way to roll back if something breaks?

Back up Apache’s original vhost files and port config before you start. If you need to revert, stop Nginx, restore Apache to listen on 80/443, restart Apache, and you’re back online.

Reverse Proxy Setup Tutorial (2026): Put Nginx in Front of Apache on a VPS for Safer SSL and Faster Static Files | HostMyCode