Home Knowledge base Skyline Cloud How to Set Up Nginx as a Reverse Proxy KNOWLEDGE BASE

How to Set Up Nginx as a Reverse Proxy

A complete, accurate guide to configuring Nginx as a reverse proxy on a Linux server — proxy headers, WebSockets, HTTPS, load balancing, and caching, with production-ready config you can copy.

What a Reverse Proxy Does

A reverse proxy sits in front of one or more backend applications and forwards client requests to them. Instead of exposing your app server (Node.js, Python, PHP-FPM, a Docker container) directly to the internet, clients talk to Nginx, and Nginx talks to your app over a private address.

This gives you a single public entry point where you can terminate TLS, balance traffic across multiple backends, cache responses, enforce rate limits, and hide internal topology. It is the standard pattern for production web apps — and it pairs naturally with a VPS or cloud server from Skyline Cloud, where your data stays in-Kingdom for PDPL and NCA alignment.

This guide uses Ubuntu/Debian paths, but every directive applies identically on RHEL-family systems.

Prerequisites

  • A Linux server with sudo access (an Skyline Cloud VPS or cloud server works well).
  • A backend app already running and listening locally — for example on 127.0.0.1:3000.
  • A domain (such as a .sa domain) with an A record pointing to your server's public IP.

Step 1 — Install Nginx

sudo apt update
sudo apt install nginx -y

Confirm it is running and enabled at boot:

sudo systemctl status nginx
sudo systemctl enable nginx

Open the firewall if you use UFW:

sudo ufw allow 'Nginx Full'

Step 2 — Create the Reverse Proxy Server Block

Nginx loads any file ending in .conf from /etc/nginx/conf.d/, and on Debian/Ubuntu it also reads /etc/nginx/sites-enabled/. Create a dedicated file so you do not touch the default:

sudo nano /etc/nginx/conf.d/app.conf

Add the following, replacing app.example.com and the backend address with your own:

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

    location / {
        proxy_pass http://127.0.0.1:3000;
        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_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
    }
}

Why each header matters

Directive Purpose
proxy_pass The backend address Nginx forwards to. No trailing slash here keeps the original URI path.
Host Passes the original hostname so the app generates correct links and routes by domain.
X-Real-IP The client's real IP, which the proxy would otherwise hide.
X-Forwarded-For Appends each proxy hop so the app sees the full client chain.
X-Forwarded-Proto Tells the app whether the original request was http or https — essential once you add TLS.

Step 3 — Test and Reload

Always validate the configuration before applying it. This catches syntax errors before they take your site down:

sudo nginx -t

You should see syntax is ok and test is successful. Then reload — reload re-reads config gracefully with near-zero interruption, unlike restart:

sudo systemctl reload nginx

Visit http://app.example.com and you should see your backend app served through Nginx.

Step 4 — Support WebSockets

If your app uses WebSockets (chat, live dashboards, dev servers), you must forward the Upgrade and Connection headers, or the connection will fail to switch protocols. Add these inside the same location block:

        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";

The proxy_http_version 1.1; line from Step 2 is required for this to work.

Step 5 — Add HTTPS with a Free Certificate

Terminate TLS at Nginx so the backend only handles plain HTTP internally. Install Certbot and let it configure SSL automatically:

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

Certbot edits your server block, adds a listen 443 ssl server, and sets up an automatic renewal timer. Verify renewal works with a dry run:

sudo certbot renew --dry-run

To enable HTTP/2, change the listen line in the SSL block to the cross-version form:

    listen 443 ssl http2;

Use listen 443 ssl http2; rather than the separate http2 on; directive — the combined form works on all current Nginx versions.

Step 6 — Load Balance Across Multiple Backends

To distribute traffic across several app instances, define an upstream group and proxy to it by name. Place the upstream block at the top of the file, outside server:

upstream backend_pool {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002 backup;
    keepalive 32;
}

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://backend_pool;
        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        "";
    }
}
  • least_conn sends each request to the backend with the fewest active connections. The default (no directive) is round-robin.
  • backup marks a server used only when the others are down.
  • keepalive 32 reuses upstream connections. When using it, set proxy_set_header Connection ""; to avoid closing them per request.

Step 7 — Cache Backend Responses (Optional)

Caching reduces load on your app for content that does not change per user. Define the cache store in the http context (for example in /etc/nginx/nginx.conf):

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

Then reference it inside the location block:

        proxy_cache       app_cache;
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        add_header X-Cache-Status $upstream_cache_status;

The X-Cache-Status response header reports HIT, MISS, or BYPASS, so you can confirm caching from your browser's network tab.

Troubleshooting

  • 502 Bad Gateway — the backend is not reachable. Confirm it is listening with ss -tlnp | grep 3000 and that the address in proxy_pass matches.
  • 504 Gateway Timeout — a slow backend; raise proxy_read_timeout.
  • App generates http:// links behind HTTPS — make sure X-Forwarded-Proto $scheme is set and your app trusts it.
  • Check the logs: sudo tail -f /var/log/nginx/error.log.

Wrapping Up

You now have a production-grade reverse proxy: TLS termination, correct client headers, WebSocket support, load balancing, and optional caching. Run it close to your users and your data — Skyline Cloud hosts everything in-Kingdom with PDPL/NCA alignment, local Arabic support, and transparent pricing. Pair this proxy with our managed business email hosting for a complete stack, and explore more guides in our Saudi web hosting hub.

Ready to deploy? Create your Skyline Cloud account and spin up a VPS in minutes.

SKYLINE Engineering

@skyline

The engineering team at SKYLINE Industrial Solutions. We publish field-tested guides drawn from real KSA and GCC deployments.

See author profile
SKYLINE engineering services

Need this implemented for you?

Reading is free — building it right takes a team. SKYLINE engineers ship Skyline Cloud for Aramco vendors, banks, hospitals and government agencies across Saudi Arabia. Talk to us before you start.

Aramco Approved Contractor ISO 9001 · ISO 27001 SAMA CSF aligned NCA ECC ready 247+ KSA clients

Comments

0 total · 0 threads
Be the first to leave a comment.