Back to tutorials
Tutorial

Nginx Reverse Proxy Setup Guide Tutorial (2026): Secure SSL, Caching, and WebSocket Support on a VPS

Nginx reverse proxy setup guide tutorial for 2026: SSL, HTTP/2, WebSockets, caching, and safe headers on your VPS.

By Anurag Singh
Updated on Jun 01, 2026
Category: Tutorial
Share article
Nginx Reverse Proxy Setup Guide Tutorial (2026): Secure SSL, Caching, and WebSocket Support on a VPS

A reverse proxy lets a VPS behave like a production edge layer without changing your application. This Nginx reverse proxy setup guide tutorial shows a clean, maintainable Nginx config. You’ll add HTTPS (with HTTP/2), WebSockets, static-asset caching, practical security headers, and debuggable logs. All commands are tested for Ubuntu and Debian in 2026.

The examples assume Ubuntu 24.04 LTS (or Debian 12) with Nginx 1.24+ from distro packages. The same layout works on dedicated servers.

On larger machines, you’ll tune worker limits, buffers, and timeouts for higher concurrency.

What you’ll build (and why it’s a common VPS pattern)

You’ll end up with this request flow:

  • InternetNginx (TLS termination + HTTP/2 + headers + caching)
  • NginxUpstream app (Node/Python/PHP-FPM/anything on localhost:3000, 8080, etc.)

This pattern gives you one public entry point for one or many apps. It also centralizes TLS and lets you rate-limit or block junk traffic in one place.

You also get a caching layer. With caching, many asset requests never reach your backend.

If you don’t already have a server, start with a HostMyCode VPS. If you’d rather not spend weekends on patching, monitoring, and routine break/fix, managed VPS hosting is the pragmatic choice.

Prerequisites checklist

  • A VPS or dedicated server with a public IP
  • A domain name pointing to that IP (A/AAAA records)
  • Root access (or sudo user)
  • Your backend app running locally (example: 127.0.0.1:3000)

If you need a refresher on DNS (A/AAAA/CNAME, TTL, and zone choices), see HostMyCode’s guide on DNS Management for VPS Hosting in 2026.

Step 1: Install Nginx and open the firewall

On Ubuntu/Debian:

sudo apt update
sudo apt install -y nginx

Confirm Nginx is running:

systemctl status nginx --no-pager

If you use UFW, allow SSH and web traffic:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Pitfall: Don’t expose your backend port (3000/8080) to the internet. Bind it to 127.0.0.1 or a private interface. Keep the firewall tight.

Step 2: Create a clean server block layout

Nginx on Ubuntu/Debian typically uses:

  • /etc/nginx/nginx.conf (global)
  • /etc/nginx/sites-available/ and /etc/nginx/sites-enabled/ (vhosts)

Create a new site config. Replace example.com with your domain:

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

Start with a minimal HTTP-only reverse proxy. You’ll add HTTPS in the next step.

upstream app_example {
  server 127.0.0.1:3000;
  keepalive 32;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;

  access_log /var/log/nginx/example.com.access.log;
  error_log  /var/log/nginx/example.com.error.log;

  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;

    proxy_set_header Connection "";
    proxy_buffering on;

    proxy_pass http://app_example;
  }
}

Enable the site and disable the default if you don’t need it:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo rm -f /etc/nginx/sites-enabled/default

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Quick smoke test from your laptop:

curl -I http://example.com

Step 3: Add Let’s Encrypt SSL (HTTPS + auto-renew)

Use Certbot with the Nginx plugin:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot will offer to redirect HTTP to HTTPS. Enable the redirect. It avoids duplicate content and keeps cookies out of cleartext.

Verify renewal:

sudo certbot renew --dry-run

If you want a deeper, screenshot-friendly walkthrough for the same flow, HostMyCode also publishes SSL Certificate Setup Guide (Tutorial) for Ubuntu VPS: Nginx + Let’s Encrypt in 2026.

Step 4: Harden TLS and add security headers (without breaking your app)

Certbot’s defaults are a good baseline. Still, it’s worth tightening the edge.

Create a reusable snippet for headers:

sudo nano /etc/nginx/snippets/security-headers.conf
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;

# HSTS: enable only after you confirm HTTPS is stable.
# Start with a low max-age; raise later.
add_header Strict-Transport-Security "max-age=86400; includeSubDomains" always;

Now update your TLS server block (Certbot created one). In your /etc/nginx/sites-available/example.com, inside the server { listen 443 ssl; ... } block, include the snippet:

include /etc/nginx/snippets/security-headers.conf;

CSP warning: Content-Security-Policy is useful, but it’s easy to get wrong. If you need it, start in Report-Only. Review reports, then enforce.

Reload safely:

sudo nginx -t
sudo systemctl reload nginx

Step 5: Make WebSockets work (the usual “it loads but doesn’t connect” bug)

If your app uses WebSockets (admin dashboards, chat, notifications), you must forward the upgrade headers.

Add this map block once in /etc/nginx/nginx.conf inside the http {} section (not inside a server block):

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

Then, in the site’s location / block, add:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

Reload and verify that your WebSocket endpoint (example path /socket) stays connected.

If you can’t test in a browser quickly, this is a decent sanity check:

curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: example.com" http://127.0.0.1/

That curl won’t complete a proper handshake like a real client. It often catches header mistakes early.

Step 6: Add static asset caching (fast wins without touching your code)

If you serve static files through Nginx (recommended), you can cache aggressively and cut repeat bandwidth.

If your static assets live in /var/www/example.com/static:

sudo mkdir -p /var/www/example.com/static

Add a dedicated location block above your proxy:

location /static/ {
  alias /var/www/example.com/static/;
  access_log off;

  # Cache static assets in browsers for 7 days
  expires 7d;
  add_header Cache-Control "public, max-age=604800, immutable";
}

Practical rule: Only use immutable if filenames are fingerprinted (for example, app.9f3c1.js). If you overwrite app.js in place, skip immutable.

Step 7: Set sane proxy timeouts and buffers (avoid random 502/504)

Defaults can be too short for slow upstream responses. On a small VPS, they can also waste memory.

Add these inside your location / block as a starting point:

# Timeouts
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

# Buffers (keep modest on small VPS)
proxy_buffering on;
proxy_buffers 16 16k;
proxy_buffer_size 16k;

If you accept uploads, also set:

client_max_body_size 50m;

Put that in the server {} block (not inside location). That way it applies consistently.

Step 8: Add basic rate limiting for login pages and APIs

This won’t replace application-level controls. It does cut down on brute-force noise.

Define a limit zone once in /etc/nginx/nginx.conf inside http {}:

limit_req_zone $binary_remote_addr zone=login_limit:10m rate=10r/m;

Then apply it to sensitive routes (example: /wp-login.php or /login):

location = /login {
  limit_req zone=login_limit burst=20 nodelay;
  proxy_pass http://app_example;
  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;
}

Tip for WordPress: Consider limiting /xmlrpc.php if you don’t need it. Many sites can disable it. Confirm your plugins won’t break first.

Step 9: Keep logs usable (and fix the “everything is 127.0.0.1” problem)

Once Nginx sits in front, your app must trust X-Forwarded-For and X-Forwarded-Proto. If it doesn’t, you’ll log fake client IPs.

You can also end up with the wrong scheme in redirects and generated links.

On the Nginx side, use a log format that shows upstream timing and addresses. Add this to /etc/nginx/nginx.conf inside http {}:

log_format proxy_main '$remote_addr - $host [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" rt=$request_time '
                      'urt=$upstream_response_time uaddr=$upstream_addr';

Then in your server block:

access_log /var/log/nginx/example.com.access.log proxy_main;

To tail errors while you test:

sudo tail -f /var/log/nginx/example.com.error.log

Step 10: Validate the setup with a tight, practical checklist

  • Nginx config passes: nginx -t returns OK
  • HTTP redirects to HTTPS: curl -I http://example.com shows 301/308 to https
  • TLS works: curl -I https://example.com returns 200/302 as expected
  • WebSockets: your app’s realtime feature connects and stays connected
  • Static cache: Cache-Control header appears on /static/*
  • Upload size: large file uploads don’t hit 413 Request Entity Too Large
  • Rate limiting: repeated login requests start returning 429 (or delayed)

Common troubleshooting (fast diagnostics that save time)

502 Bad Gateway

  • Upstream app isn’t running or listening on the expected port.
  • App listens on localhost but you proxy to 0.0.0.0 or wrong IP.
sudo ss -lntp | grep -E ':3000|:8080'
sudo journalctl -u nginx --no-pager -n 100
sudo tail -n 100 /var/log/nginx/example.com.error.log

Infinite redirect loop after enabling HTTPS

  • Your app thinks requests are HTTP because it ignores X-Forwarded-Proto.

Fix: confirm Nginx sets X-Forwarded-Proto. Then configure your app/framework to trust proxy headers.

WebSocket connects then drops

  • Missing Upgrade/Connection headers.
  • Proxy timeouts too low for idle connections.

Fix: add the map + headers and raise proxy_read_timeout for the WebSocket location (often 300s+).

413 Request Entity Too Large

Fix: set client_max_body_size in the server block and reload Nginx.

Optional: hosting multiple apps/domains on one VPS

The pattern doesn’t change. Use one server {} per domain, and point each one at its own upstream.

Keep each file small and name things clearly. Resist the urge to build a “mega config.”

If you manage many client domains, a control panel can pay for itself quickly. For resellers, reseller hosting covers the usual workflows (accounts, email, SSL, DNS pointers) without you editing vhost files all day.

Where HostMyCode fits (practical hosting choices)

For one or two apps, a VPS usually hits the sweet spot: cost, control, and decent performance. If you need steadier performance under sustained load—or you’re running lots of sites—move up to a dedicated server.

If you want fewer operational chores, managed plans reduce the day-to-day babysitting.

If you’re putting Nginx in front of production traffic, start with a HostMyCode VPS so you can control Nginx, certificates, and firewall rules. If you don’t want to track updates, security patches, and service health yourself, managed VPS hosting is a better fit for long-running web apps.

FAQ

Should I run my app on 0.0.0.0 or 127.0.0.1 behind Nginx?

Use 127.0.0.1 (or a private interface) whenever possible. It prevents accidental public exposure of your app port and makes firewall rules simpler.

Do I need HTTP/3 for a VPS reverse proxy in 2026?

It’s optional. Many sites do fine on HTTP/2. If you want HTTP/3/QUIC, you’ll typically use a newer Nginx build or an alternative stack. Start with HTTP/2 and measure.

Can I use this setup for WordPress?

Yes, but WordPress usually runs best with Nginx + PHP-FPM (or a control panel that manages it). If you’re primarily hosting WordPress, consider WordPress hosting instead of maintaining the full stack yourself.

What’s the safest way to reload Nginx during changes?

Run nginx -t first. If it passes, use systemctl reload nginx. Reload is less disruptive than restart because it keeps existing connections alive.

How do I migrate an existing site to this reverse proxy without downtime?

Bring up the new VPS, configure Nginx + SSL, test using a temporary hosts-file override, then cut DNS with a low TTL. If you want help moving sites cleanly, use HostMyCode migrations.

Summary

This tutorial built a practical Nginx reverse proxy: Let’s Encrypt TLS termination, safer headers, WebSocket support, basic rate limiting, static caching, and logs you can debug quickly. The config stays small, predictable, and easy to extend as you add more apps.

If you’re ready to run real traffic, deploy it on a HostMyCode VPS (or move to managed if you want fewer maintenance tasks). Either way, the reverse proxy pattern keeps deployments cleaner and upgrades easier to reason about.