Quick answer: To use NGINX as a reverse proxy, point a controlled server/location block at your application with proxy_pass, define which request headers the application may trust, test the configuration and reload NGINX. Verify the application response through NGINX—not just nginx -t or a welcome page. A reverse proxy handles traffic entering your application; Socks5.IO is a separate option for the application’s authorized outbound connections.
This tutorial builds a small, local HTTP lab on an Ubuntu/Debian-style installation: NGINX receives requests on 127.0.0.1:8080 and forwards them to a demonstration application on 127.0.0.1:3000. It does not require a public domain, change your default website or publish an open proxy. Complete the local checks before planning public HTTPS deployment.
How does NGINX reverse proxy work?
NGINX accepts a client connection, chooses a configured virtual host and location, sends a request to the selected upstream application, and returns its response. The client need not know the backend’s port or address. The official reverse-proxy guide explains this intermediary role.
Request: client -> NGINX listener -> application backend
Response: client <- NGINX listener <- application backend
This lab:
curl -> 127.0.0.1:8080, Host proxy-demo.test -> 127.0.0.1:3000

Common uses include routing several application paths, terminating client TLS, distributing requests across backends and applying an intentional cache policy. These are configured capabilities, not automatic outcomes of adding proxy_pass. A reverse proxy does not remove application vulnerabilities, provide a WAF by default or ensure that the origin cannot be reached through another route.
Reverse proxy versus forward proxy
| Question | NGINX reverse proxy in this guide | Application outbound proxy |
|---|---|---|
| Who connects first? | A client calling your application | Your application calling an external source |
| What is selected? | A backend application you administer | A supported egress gateway and destination |
| Who configures it? | The application operator configures NGINX | The collecting application/client configures its proxy |
| What does success prove? | A request reached the intended backend through this listener | A specific client connection used a verified route |
A forward proxy can change the source address observed by a destination. That is not an end-to-end anonymity guarantee. The reverse proxy, egress service and destination can each observe parts of the flow, and neither direction grants permission to access data.
How to set up NGINX reverse proxy on Ubuntu or Debian
Use a disposable development server or an approved staging host with sudo access. The steps assume a systemd-managed Ubuntu/Debian package layout with /etc/nginx/sites-available and sites-enabled. Other distributions, containers and managed hosting may use different includes and service commands.
Do not overwrite an existing production site. Check that local ports 3000 and 8080 are free; if not, choose alternatives and update every related configuration and command consistently. These Bash commands are for the Linux host, not Windows PowerShell.
1. Install the tools and inspect the existing service
sudo apt update
sudo apt install nginx python3 curl
nginx -v
sudo nginx -t
sudo systemctl status nginx --no-pager
sudo ss -ltnp '( sport = :3000 or sport = :8080 )'
Ubuntu’s installation documentation describes the package installation. Installation may start the default service; understand that behavior before using a shared host. If NGINX is stopped, first inspect why, then start it with sudo systemctl start nginx when appropriate. Do not disable firewalls or stop another application’s process to make the lab fit.
2. Run a backend with an identifiable response
In a working directory owned by your ordinary user, save the following as proxy_demo_backend.py. It serves one diagnostic endpoint and deliberately binds only to loopback.
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if urlsplit(self.path).path != "/healthz":
self.send_error(404)
return
payload = {
"backend": "proxy-demo-backend",
"path": self.path,
"host": self.headers.get("Host"),
"x_real_ip": self.headers.get("X-Real-IP"),
"x_forwarded_for": self.headers.get("X-Forwarded-For"),
"x_forwarded_proto": self.headers.get("X-Forwarded-Proto"),
"forwarded": self.headers.get("Forwarded"),
"request_id": self.headers.get("X-Request-ID"),
}
body = json.dumps(payload).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass
if __name__ == "__main__":
ThreadingHTTPServer(("127.0.0.1", 3000), Handler).serve_forever()
In terminal A, start it and leave it running:
python3 proxy_demo_backend.py
In terminal B, check the backend directly:
curl --noproxy '*' --fail-with-body --silent --show-error \
http://127.0.0.1:3000/healthz
Pass condition: JSON contains backend=“proxy-demo-backend” and path=“/healthz”. Forwarded values are normally null because this direct baseline did not supply them. This Python server is a teaching backend, not a production application server or readiness check for real dependencies.
3. Add a separate NGINX lab site
Use sudoedit /etc/nginx/sites-available/proxy-demo to create a new file. If it already exists, inspect it and choose another name rather than replacing someone else’s configuration. Paste this complete fragment; the distribution includes enabled site files inside its http context.
# /etc/nginx/sites-available/proxy-demo
# This fragment belongs inside the existing http context, not at top level.
log_format proxy_demo '$request_id $remote_addr $request_method $uri '
'status=$status upstream=$upstream_addr '
'upstream_status=$upstream_status '
'upstream_time=$upstream_response_time total=$request_time';
server {
listen 127.0.0.1:8080 default_server;
server_name _;
access_log off;
return 404;
}
server {
listen 127.0.0.1:8080;
server_name proxy-demo.test;
access_log /var/log/nginx/proxy-demo.access.log proxy_demo;
error_log /var/log/nginx/proxy-demo.error.log warn;
add_header X-Request-ID $request_id always;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Forwarded "";
proxy_connect_timeout 3s;
proxy_read_timeout 15s;
proxy_send_timeout 15s;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
The upstream is the local application, not an egress proxy gateway. The no-URI form of proxy_pass preserves the incoming path in this simple location. The catch-all server rejects unknown hostnames on this dedicated loopback listener. It does not change how your other listeners or websites handle requests.

The explicit HTTP/1.1 setting avoids relying on defaults across installed versions. The timeout values are lab choices: read/send timeouts govern gaps between successive operations, not an overall application deadline. Check the proxy module reference before adapting protocol and timeout behavior.
4. Enable, test and reload the lab
After checking that no file or link with the same enabled name exists:
sudo ln -s /etc/nginx/sites-available/proxy-demo /etc/nginx/sites-enabled/proxy-demo
sudo nginx -t
Do not run the link command repeatedly or add force flags to overwrite an existing entry. If the configuration test fails, fix the reported problem before proceeding. A normal passing test reports syntax is ok and test is successful; it checks configuration loading, not a live connection to the backend.
sudo systemctl reload nginx
sudo systemctl status nginx --no-pager
Reload rather than restarting a healthy service merely to apply a site change. The NGINX beginner’s guide describes its reload lifecycle. A successful command alone still does not establish that your request matches the intended virtual host.

How to check if NGINX reverse proxy is working
Use a layered test: direct backend, selected proxy listener, response content and correlated logs.
Verify the backend through the configured virtual host
curl --noproxy '*' --fail-with-body --silent --show-error --include \
--resolve proxy-demo.test:8080:127.0.0.1 \
http://proxy-demo.test:8080/healthz
The resolve option supplies the test address without editing DNS or a hosts file. The no-proxy option deliberately excludes ambient forward proxies from this local check.
Pass conditions:
- HTTP status is 200 and the body identifies proxy-demo-backend.
- path is /healthz, host is proxy-demo.test and x_forwarded_proto is http in this HTTP lab.
- x_real_ip and x_forwarded_for are the actual loopback peer, not a supplied visitor header.
- The response’s X-Request-ID matches request_id in the backend JSON and the dedicated access-log entry.
Host is set from $host, which does not preserve the client’s port. An application generating absolute URLs needs an explicitly designed canonical host/port policy; do not assume this diagnostic example configures every framework’s URL generation correctly.

Check the header trust boundary
Repeat the request with deliberately fake incoming forwarding headers:
curl --noproxy '*' --fail-with-body --silent --show-error \
--resolve proxy-demo.test:8080:127.0.0.1 \
-H 'X-Forwarded-For: 198.51.100.77' \
-H 'X-Real-IP: 198.51.100.77' \
-H 'X-Forwarded-Proto: https' \
-H 'Forwarded: for=198.51.100.77;proto=https' \
http://proxy-demo.test:8080/healthz
Pass condition: the backend still receives loopback client addresses and http, while forwarded is null. The outer trusted edge overwrites the forwarded chain here rather than appending untrusted visitor input. Configure the real application to trust only the headers and proxy hops you administer; these fields are not authentication credentials.

Figure 5. Actual header-test observations, typeset. This direct-edge policy is not a ready-made CDN trust configuration.
If a trusted CDN or load balancer sits in front, this direct-edge policy needs redesign. Configure trusted source ranges and the appropriate header using the real-IP module, where available, and prevent clients from bypassing that edge. Never trust all Internet addresses to declare their own client identity. Likewise, do not overwrite an original HTTPS scheme with an internal HTTP hop without a trusted scheme policy.
Inspect the logs and test failure deliberately
sudo tail -n 20 /var/log/nginx/proxy-demo.access.log
sudo tail -n 20 /var/log/nginx/proxy-demo.error.log
The dedicated access log includes upstream address/status and response timing, making it more informative than a bare 200. It omits query strings, but paths, addresses and error logs can still contain sensitive information. Restrict access and retention; redact evidence before publication.

On this lab only, stop the demonstration backend with Ctrl+C in terminal A and repeat the proxy curl command. A connection failure to the now-absent backend should produce 502 and a corresponding upstream connection error. Restore the backend with python3 proxy_demo_backend.py and confirm 200 returns. Do not stop a production application to reproduce this exercise.

An unexpected-host check should return 404 instead of reaching the app:
curl --noproxy '*' --silent --show-error --include \
-H 'Host: other.example' http://127.0.0.1:8080/healthz
This verifies the lab listener’s host routing, not TLS or public network reachability. curl -I uses HEAD; this tiny teaching app implements GET, so use the commands above rather than treating an unsupported HEAD response as a broken proxy.
Verification scope: The article’s backend and site fragment passed twelve local checks using official portable NGINX 1.31.8 on Windows, with a private top-level configuration wrapping the same site fragment. Checks covered responses, header replacement, host rejection, request/log correlation, path-prefix behavior and 502 recovery. Ubuntu package installation/systemd commands, public DNS/TLS/firewall behavior and a real Socks5.IO route were not exercised by that loopback test.
Understand proxy_pass paths before adding /api/
For an incoming /api/healthz?check=1, these simple prefix locations differ:
| Location and upstream | Backend path |
|---|---|
| location /api/ with proxy_pass http:// 127.0.0.1:3000; | /api/healthz?check=1 |
| location /api/ with proxy_pass http:// 127.0.0.1:3000/; | /healthz?check=1 |
These are explanatory alternatives, not instructions to paste two identical locations into one server. In a test branch, add one /api/ location to the named server, retain its inherited header policy, test and reload. The stripped-prefix alternative should reach this backend’s /healthz; the preserved-prefix version should return the backend’s 404. Variable-based, regex and rewrite configurations need their own path tests rather than blindly applying this table.

Production checklist: HTTPS, origins and special traffic
The loopback lab is not a public HTTPS deployment. Before publishing an application:
- Create an approved public virtual host with the real domain and intentionally selected listeners. Keep the loopback lab separate or retire its dedicated entry.
- Provision a valid certificate/private key using your organization’s supported process. Configure and verify client TLS; only redirect HTTP once the HTTPS destination works. Do not copy nonexistent certificate paths or disable certificate validation.
- Keep the backend on loopback when it shares the host, or restrict its network listener to the private proxy path. In containers, 127.0.0.1 refers to that container, not another container or automatically the host.
- If proxying across an untrusted network, secure the upstream leg too. Configure the expected TLS server name, certificate verification and a trusted CA bundle; HTTPS to the public edge does not encrypt an HTTP backend connection. See the upstream TLS guide.
- Configure framework-specific trusted-proxy, authentication, upload and timeout policies. Test redirects, secure cookies, request bodies and public client paths—not only /healthz.
For WebSockets, explicitly handle upgrade headers in the intended location and test a real upgrade. NGINX’s WebSocket documentation covers the additional handling. Streaming/SSE may require a deliberate buffering policy. Caching is not enabled by this lab; do not apply a blanket cache to authenticated or personalized responses.
For load balancing, an upstream group contains application endpoints. Session state and failure behavior still require design; adding a second address does not prove high availability. Do not insert residential proxy nodes into an application upstream group and expect them to behave as interchangeable app servers.
Can NGINX use Socks5.IO for outbound requests?
Use separate components for separate directions:
Inbound: client -> NGINX reverse proxy -> your application
Outbound: your application -> compatible Socks5.IO forward proxy -> approved external source
Ordinary HTTP proxy_pass is not a SOCKS5 client. Nor does pointing it at a commercial HTTP proxy automatically implement the forward-proxy request format or HTTPS CONNECT negotiation required by that service. An upstream group does not add that protocol support. proxy_bind selects an available local source address; it does not purchase or borrow a remote residential exit.
The correct place for an application’s egress proxy is normally its outbound HTTP/browser client or an explicitly designed, supported egress component. NGINX has other product/module capabilities, including forward-proxy features in some configurations, but those are not established by this reverse-proxy lab and are not a universal third-party egress adapter.
Where an authorized integration needs residential egress, assess Socks5.IO rotating residential proxies in that outbound component. Obtain current endpoint, protocol and session settings from your account. The residential proxy setup guide explains configuration and client-side verification; do not put proxy secrets in a public NGINX file or diagnostic response.
Choose protocol support by the actual client. Our SOCKS versus HTTP proxy guide explains that distinction; the label SOCKS5 does not make every browser or reverse proxy an authenticated SOCKS client. Verify the egress from the actual application client, not from an unrelated shell, and verify its permitted destination separately.
For example, a team may expose an internal reporting API behind NGINX while its background worker checks approved external product feeds. The reverse proxy handles incoming API traffic; the worker separately configures its outbound route, quotas and credentials. A successful inbound /healthz request says nothing about that worker’s exit IP. This is an architecture example, not a measured customer deployment or a Socks5.IO connectivity benchmark.
Troubleshooting: 502, 504, wrong content and missing client IPs
| Symptom | Check first | Avoid |
|---|---|---|
| nginx -t fails | Exact filename/line, context, duplicate listens, syntax and file permissions | Reloading anyway or publishing a secret-bearing nginx -T dump |
| 502 Bad Gateway | Backend listener, upstream protocol, connection refusal or invalid response; inspect the error log | Assuming every 502 is a firewall block |
| 504 Gateway Timeout | Which hop timed out, backend latency and the relevant timeout | Increasing all timeouts without diagnosing the slow operation |
| Welcome page or unexpected 200 | Destination address, Host, selected listener and application response body | Treating any 200 as proof of the configured route |
| 404 from /api/ | Prefix preservation versus replacement, then app route | Removing slashes at random |
| App sees NGINX’s address | Trusted proxy/header policy and direct-backend exposure | Trusting arbitrary X-Forwarded-For from visitors |
| Redirect loop or wrong scheme | TLS termination hops, trusted scheme and app canonical URL settings | Trusting a visitor’s X-Forwarded-Proto blindly |
| Public request fails but local works | DNS, selected IP/listener, approved firewall/security-group rules and TLS | Disabling the firewall or exposing port 3000 publicly |
A timeout alone does not identify a blocking firewall. Test each hop and inspect policy evidence before changing network rules. This lab needs no public UFW rule because both listeners are on loopback. Public port 80/443 access must be reviewed separately, without accidentally removing SSH access.
Conclusion
Using NGINX as a reverse proxy is straightforward when you separate listener selection, upstream routing and application trust. Build a known backend, test the configuration, reload safely, and verify the actual response plus correlated logs. Then add public HTTPS, special protocols and operational controls against explicit acceptance checks.
Keep outbound proxy routing separate. Socks5.IO can serve a compatible application’s authorized egress needs, but it is not a substitute for a backend server, and reverse-proxy health does not verify that egress path.











