• Indonesian
  • English
  • Nginx Reverse Proxy Setup: Complete Step-by-Step Guide 2026

    Kecepatan:
    ⏱ 8 min read

    Set Up Reverse Proxy Nginx on VPS: Complete Step-by-Step Production Guide

    Skip the preamble. You’ve got a VPS, a backend service stuck on some random port, and you want it reachable over a clean domain with HTTPS. No weird :8000 in the URL. No mixed-content warnings. This is the exact Nginx reverse proxy setup I push to production boxes, written as a checklist. Follow it top to bottom and you’re done.

    One assumption: Nginx is already installed and your domain’s DNS points at the VPS. If DNS hasn’t propagated yet, fix that first – five minutes now saves you twenty minutes of wondering why nothing works.

    Difficulty: Intermediate
    Last Updated: August 2026
    Tested On: Debian 12, Nginx 1.24.0

    Why even bother? Because running services on raw ports is a maintenance trap. Users don’t remember ports. Corporate firewalls block everything except 80 and 443. And every port you expose is another entry point for scanners. A reverse proxy changes the whole picture: one door in, one place to handle TLS, one place to control headers, limits and timeouts. Think of it like a reception desk – every visitor reports to one person, who routes them to the right office instead of knocking on every door.

    Here’s the architecture you’re building: internet -> Nginx on port 443 -> your app on 127.0.0.1:8000. The backend listens on loopback only, so it’s invisible from the outside. That alone is a meaningful security upgrade.

    And I can tell you from real tickets what happens without it. Bookmark files full of stale ports. Certificates expiring quietly because each service had its own SSL setup nobody tracked. Uploads failing at mysterious sizes because the app server had its own body limits. And my favorite – a 2 AM incident where nobody knew which service owned which port. Every one of those was solved by the same fifteen-minute job: point everything at Nginx and centralize. Let’s do exactly that.

    Step 1: Confirm Both Ends Are Up

    Skip this if you know both are healthy. Otherwise verify first so testing later isn’t guesswork:

    nginx -v
    systemctl is-active nginx
    curl -s http://127.0.0.1:8000/health

    The curl should return something – plain text, JSON, whatever your app serves. Connection refused means nothing is listening on that port. For a from-scratch Nginx install, check out install Nginx on Ubuntu.

    Step 2: Write the Server Block

    Back up before you edit anything. This habit has saved me more than once.

    SECURITY WARNING: Back Up Before Continuing

    Before touching Nginx config, make sure you’ve done the following:

    • Backed up nginx.conf: cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak-$(date +%F)
    • Listed the currently enabled sites so you can roll back if something breaks
    • Confirmed the subdomain you’re about to use is spelled correctly

    A broken config reload takes every site on the box down, not just the one you’re editing. Don’t skip the backup.

    Now create the server block in /etc/nginx/sites-available/:

    nano /etc/nginx/sites-available/api.exampleclient.com

    Paste this in:

    server {
        listen 80;
        listen [::]:80;
        server_name api.exampleclient.com;
    
        location / {
            proxy_pass http://127.0.0.1:8000;
            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;
        }
    }

    Save, then link it into sites-enabled, test the config, and reload:

    ln -s /etc/nginx/sites-available/api.exampleclient.com /etc/nginx/sites-enabled/
    nginx -t
    systemctl reload nginx

    nginx -t should print syntax is ok and test is successful. If it complains, look for a missing semicolon – that’s the cause nine times out of ten.

    Step 3: Add the Headers Everyone Forgets

    The base config works, but three additions save you real headaches later:

    • Upgrade and Connection – required for WebSocket backends (chat, live dashboards, terminal sessions)
    • proxy_read_timeout – for long-running requests like report generation, so you don’t eat 504s
    • client_max_body_size – so large uploads don’t silently fail at Nginx’s default 1m limit
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
    client_max_body_size 50m;

    WebSocket connections start as an HTTP Upgrade request. If Nginx doesn’t forward that header, the connection drops every few seconds. It’s the most confusing symptom you’ll see in a realtime app, and this one line fixes it.

    Step 4: Terminate TLS at Nginx

    One cert, one place, automatic renewal – that’s the whole point of centralizing. Install certbot and let it wire everything up:

    apt install certbot python3-certbot-nginx -y
    certbot --nginx -d api.exampleclient.com

    certbot reads your server block, issues the certificate, and adds the HTTPS redirect for you. Renewal runs on a systemd timer, so check it once and forget it. Full breakdown here: Let’s Encrypt SSL with Nginx.

    Step 5: Verify From the Outside

    Don’t test from inside the box. Test from a machine that actually hits your public IP:

    curl -I https://api.exampleclient.com

    Expected output:

    HTTP/1.1 200 OK
    Server: nginx/1.24.0
    Date: Tue, 04 Aug 2026 07:00:00 GMT
    Content-Type: text/plain

    If you get 200, you’re live. Open it in a browser, check the padlock, and run the main flows of your app. Done.

    When It Breaks: Read the Log, Then the Table

    Rule I drill into the team: don’t fear the error, fear the log you didn’t read. Here’s the classic failure when the backend dies, straight from /var/log/nginx/error.log:

    2026/08/04 02:11:08 [error] 14823#14823: *512 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.45, server: api.exampleclient.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "api.exampleclient.com"
    2026/08/04 02:11:09 [error] 14823#14823: *513 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.45, server: api.exampleclient.com, request: "GET /api/v1/users HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "api.exampleclient.com"
    2026/08/04 02:11:10 [error] 14823#14823: *514 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.45, server: api.exampleclient.com, request: "GET /health HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "api.exampleclient.com"

    Read the pattern, don’t skim it. The first line shows the symptom: requests reach Nginx, but Nginx can’t reach upstream. The middle lines repeat the same pattern across different paths, so it’s not a routing problem. The repeated (111) Connection refused on the final lines points straight at the root cause: nothing is listening on 127.0.0.1:8000. Fix the backend, re-run curl against localhost, and the 502s clear.

    For quick reference, here’s the troubleshooting table I keep by my desk:

    Symptom Likely Cause Quick Fix
    502 Bad Gateway Backend down or wrong port systemctl status your-service, curl the backend directly
    504 Gateway Timeout Backend too slow to respond Raise proxy_read_timeout
    Redirect loop / too many redirects App thinks it’s on plain HTTP behind TLS Make the app honor X-Forwarded-Proto
    WebSocket drops every few seconds Upgrade headers not forwarded Add proxy_set_header Upgrade and Connection
    Client IP always shows 127.0.0.1 X-Forwarded-For missing Add the header and read it in the app

    Notes From the Field

    Use 127.0.0.1 in proxy_pass, not localhost. On some builds Nginx resolves localhost to ::1 first and the connection goes nowhere. 127.0.0.1 leaves zero ambiguity.

    Keep the backend on loopback. If it’s an internal app, let it listen on 127.0.0.1 only and let Nginx handle the outside world. Fewer open ports, smaller attack surface.

    If UFW is active on the VPS, only allow ports 80 and 443. Keep backend ports closed to the internet. Set that up here: UFW firewall setup on VPS.

    Want the full picture on what your box is doing? Pair this with server monitoring with Prometheus and Grafana, and if Nginx ever feels heavy, troubleshooting Nginx high load has your back.

    Here’s what the architecture we just built looks like:

    nginx reverse proxy setup architecture

    FAQ: Nginx Reverse Proxy Setup

    Q: Does a reverse proxy slow down my site?

    Negligibly. Nginx just forwards requests; the overhead is tiny, and it’s often faster because Nginx handles TLS and static files more efficiently than your app server. If things feel slow, suspect the config or the backend, not the proxy itself.

    Q: Can one Nginx instance proxy multiple apps?

    Yes, as many as your VPS resources allow. Give each app its own server block with a unique server_name. Production boxes routinely run dozens of subdomains through a single Nginx – plan your backend ports and watch your resource usage.

    Q: Reverse proxy vs load balancer – same thing?

    Close but not identical. A reverse proxy forwards to a single backend based on config. A load balancer spreads traffic across several backends for scaling and failover. Nginx can do both – add an upstream block with multiple servers and you have a simple load balancer.

    Q: Why does my app think every request is plain HTTP?

    Because Nginx terminates TLS and forwards to the backend over plain HTTP, unless you tell the app otherwise. Forward X-Forwarded-Proto and make your framework read it, or the app will build URLs and redirects with http:// and cause redirect loops.

    Q: Should I run Nginx inside Docker instead of natively?

    Both work. Native install is simpler for a single box and has fewer networking footguns. Docker shines when you need repeatable, portable setups. Pick based on how your team manages the fleet – just don’t run both without a plan for port conflicts.

    Author: Syslog Solutions – NOC & Server Management Team. We handle 500+ servers daily, from shared hosting to enterprise dedicated infrastructure.

    That’s the whole job. Write the block, enable it, test it, add TLS. Fifteen minutes, maybe less. If you hit an error, read the log line by line before touching config – the pattern is always there. Bookmark this if you plan to add more services; you’ll reuse this exact template. Now go wire up that server block.