How to set up Nginx as a reverse proxy
A reverse proxy is a server that sits between visitors and your application: it accepts the request from the internet, forwards it to the application behind it and sends the application's response back to the visitor. The visitor only ever sees the reverse proxy and does not know which port or language the application runs on.
Applications written in Node.js, Python, Java, .NET or Go usually run their own small web server on a port such as 3000 or 8080. Putting Nginx in front of such an application, rather than exposing it directly to the internet, lets you serve several applications from one address, manage HTTPS in one place, serve static files quickly and set limits such as request size and timeouts centrally. For the concept itself, see the guide on what a reverse proxy is.
This guide builds a working configuration for a single backend server, step by step. The examples assume the domain example.com and the application address 127.0.0.1:3000.
In brief
- Nginx accepts the request and forwards it to the application behind it with proxy_pass.
- A trailing slash in proxy_pass changes the path sent to the backend.
- Without Host and X-Forwarded-* headers the application cannot see the real IP or https.
- After every change, test with nginx -t, then reload.
What you need
- A server with Nginx installed and administrator (sudo) rights
- An application running on the server and the port it listens on (127.0.0.1:3000 in the examples)
- A domain name pointing to the server's IP address
- An SSH session in which you can edit the configuration files
Note
The examples show port 80 (HTTP) only. A live site also needs HTTPS; the certificate and port 443 settings are covered in a separate guide.
On this page
Step-by-step setup
-
Make the application listen on the local address only
If Nginx and the application are on the same server, make the application listen on
127.0.0.1rather than0.0.0.0(all network interfaces). Only Nginx on the same server can then reach it; nobody can bypass Nginx and connect directly athttp://server-ip:3000.If the application is on another server, bind it to the internal network address (e.g.
10.0.0.5) and allow only the Nginx server to reach that port in the firewall. This step is also the precondition for the "real IP" setting described later being safe.
: Enlarge -
Create the server block
In Nginx every site is defined by a
serverblock. Create a new configuration file. Its location depends on the distribution:.conffiles under/etc/nginx/conf.d/are a common layout; Debian and Ubuntu use/etc/nginx/sites-available/together withsites-enabled/.Nginx# Example file: /etc/nginx/conf.d/example.com.conf (the path depends on the distribution) server { listen 80; server_name example.com www.example.com; location / { # Forward all requests to the application behind Nginx proxy_pass http://127.0.0.1:3000; } }listensets the port to listen on andserver_namesets which domain names this block answers for. Thelocation /block covers every address; theproxy_passdirective inside it says where the request is forwarded. -
Use proxy_pass and the trailing slash correctly
This is where the most common mistake happens. If the
proxy_passaddress has a path after the server name (even a single/), Nginx replaces the part of the request that matched thelocationwith that path. If there is no path, the request address is passed on unchanged.location proxy_pass Incoming request Path sent to the backend /app/http://127.0.0.1:3000/app/login/app/login/app/http://127.0.0.1:3000//app/login/login/old/http://127.0.0.1:3000/new//old/page/new/page/apphttp://127.0.0.1:3000//app/login//login(wrong)Nginx# 1) NO path in proxy_pass: the address is passed on unchanged # /app/login -> /app/login location /app/ { proxy_pass http://127.0.0.1:3000; } # 2) proxy_pass ends with "/": the matched prefix is removed # /api/list -> /list location /api/ { proxy_pass http://127.0.0.1:3001/; } # 3) proxy_pass has another path: the prefix is replaced by that path # /old/page -> /new/page location /old/ { proxy_pass http://127.0.0.1:3000/new/; }As a rule, end the
locationand theproxy_passpath the same way: both with a slash, or no path at all inproxy_pass. If your application is configured to run under a sub-path (e.g./app/), keep the prefix; if it assumes it runs at the root, strip it. Inlocationblocks defined by a regular expression,proxy_passcannot contain a path; Nginx reports an error in the configuration test.
: Enlarge -
Forward the necessary headers
Nginx sends the request to the backend again on its own behalf. Without extra settings the application sees Nginx's IP address instead of the visitor's, and the
Hostheader becomes the address written inproxy_pass. These four headers carry the missing information to the application:Nginxproxy_pass http://127.0.0.1:3000; # The domain name the visitor asked for proxy_set_header Host $host; # IP address of the client that connected to Nginx proxy_set_header X-Real-IP $remote_addr; # Addresses the request passed through (appended to the existing list) proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Scheme used by the visitor: http or https proxy_set_header X-Forwarded-Proto $scheme;- Host: the domain name the visitor asked for. The application uses it to generate links and to tell several domains apart.
- X-Real-IP: the IP address of the client that connected to Nginx.
- X-Forwarded-For: the list of addresses the request has passed through.
$proxy_add_x_forwarded_forappends the connecting address to the list sent by the client. - X-Forwarded-Proto: the scheme the visitor used (
httporhttps).
Note:
proxy_set_headerdirectives are inherited from the level above only if the current level has noproxy_set_headerat all. If you add a single header inside alocation, the ones you wrote atserverlevel no longer apply in that block; you have to repeat all of them. -
Add WebSocket support
Features such as live chat, notifications or real-time dashboards use WebSocket. A WebSocket starts as an ordinary HTTP request that is upgraded to a persistent connection through the
Upgradeheader. These headers do not pass through a proxy on their own; they have to be forwarded explicitly, and Nginx has to speak HTTP/1.1 to the backend.Nginx# Defined once at http level (outside the server block) map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:3000; # HTTP/1.1 and upgrade headers for WebSocket proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; 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; # An idle WebSocket connection is closed after this time proxy_read_timeout 300s; } }The
mapblock belongs athttplevel (outside the server block): it sendsConnection: upgradeif the client asked for an upgrade andConnection: closeif it did not. The samelocationtherefore works for ordinary requests and for WebSocket.If no data arrives on a WebSocket connection for the duration of
proxy_read_timeout, Nginx closes it. Increase the value or make the application send a "ping" at regular intervals.
: Enlarge -
Set the timeouts and the body size
Directive What does it set? Default proxy_connect_timeoutHow long to wait for a connection to the backend to be established 60s proxy_send_timeoutThe longest wait between two write operations while sending the request to the backend 60s proxy_read_timeoutThe longest wait between two read operations while reading the response from the backend 60s client_max_body_sizeThe upper limit for the request body a client may send (e.g. a file upload) 1m Nginx# Upload limit (default 1m); exceeding it returns 413 client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:3000; proxy_connect_timeout 5s; # connecting to the backend proxy_send_timeout 60s; # sending the request to the backend proxy_read_timeout 60s; # waiting for the backend's response } # Longer wait for the long-running report address only location /reports/ { proxy_pass http://127.0.0.1:3000; proxy_read_timeout 180s; }The read and send timeouts measure the silence between two operations, not the total duration. If the application sends nothing for 60 seconds, the visitor gets a 504 error. If the upload limit is exceeded, Nginx returns a 413 error; remember to set the application's own upload limit to the same value. Do not make the timeouts longer than necessary: writing a dedicated
locationfor specific addresses such as long reports or exports is better than raising the limit for the whole site. These errors are covered in detail in the guide on 502, 504 and other Nginx errors. -
Test the configuration and reload
Nginx does not re-read its configuration files on its own. Test the syntax first and, if the test succeeds, reload. A reload does not drop open connections; if the configuration is invalid, Nginx keeps running with the old settings.
Bash# 1) Test the configuration sudo nginx -t # 2) If the test succeeds, reload (connections are not dropped) sudo systemctl reload nginx # The same on systems without systemd: sudo nginx -s reloadnginx -tonly checks the syntax and the directive context; it does not check whether the backend is up. So try the site afterwards withcurlor in a browser.
: Enlarge -
Teach the application to trust the proxy
Even when Nginx sends the headers, the application does not use them automatically. Most application frameworks have a setting for this called "trusted proxies": you enter the address of your reverse proxy (
127.0.0.1if it is on the same server), and the framework then honours theX-Forwarded-*headers only for requests coming from that address. The name and place of the setting depend on the framework you use; look for "proxy" in its documentation.Typical symptoms when this setting is missing: every visitor shows up as
127.0.0.1in the logs, IP-based rate limiting counts everybody as one person, the application believes it is onhttpand falls into an endless redirect loop, or it generates links withhttp://.Security rule: anyone can send
X-Forwarded-*headers. Trust them only when the request comes from your own reverse proxy; avoid settings of the "trust all addresses" kind. Otherwise an attacker can bypass IP restrictions and rate limits with a forged header. In a PHP application without a framework, the same logic looks like this:PHP<?php // Addresses of your own reverse proxy only (127.0.0.1 if Nginx is on the same server) $guvenilenProxyler = array('127.0.0.1'); $uzakAdres = $_SERVER['REMOTE_ADDR'] ?? ''; $istemciIp = $uzakAdres; $httpsMi = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; // The headers are read only if the request came from a trusted proxy if (in_array($uzakAdres, $guvenilenProxyler, true)) { $gercekIp = $_SERVER['HTTP_X_REAL_IP'] ?? ''; if (filter_var($gercekIp, FILTER_VALIDATE_IP) !== false) { $istemciIp = $gercekIp; } $httpsMi = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; }The example reads the
X-Real-IPheader because Nginx overwrites it on every request with the address it actually saw.X-Forwarded-For, by contrast, appends to the value sent by the client; the first address in the list may be forged, and the trustworthy one is the last address, added by your proxy.
: Enlarge
Complete example configuration
The file below brings all the previous steps together. Replace the domain name and the application address with your own.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name example.com www.example.com;
# Upper limit for the request body (file uploads)
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# So that the application sees the real domain, IP and scheme
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
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}To add HTTPS, continue with the guide on HTTPS in Nginx. Once HTTPS is enabled, the $scheme variable becomes https by itself; you do not need to change the header lines. To distribute requests across several application servers, see the guide on load balancing.
Common mistakes
- Slash mismatch:
location /apiis combined withproxy_pass …/; the backend receives//pathor the application returns 404. - Forgetting the Host header: the application generates links with
127.0.0.1:3000or shows the wrong site. - Writing a single header inside a location: the other
proxy_set_headerlines from the level above are not inherited in that block. - Forgetting WebSocket: the page loads but live features do not work; the browser console shows a connection error.
- Leaving the backend open to the internet: if the application listens on
0.0.0.0, all limits and access rules in Nginx can be bypassed. - Restarting without testing: a restart with a broken configuration can leave Nginx unable to start at all. Run
nginx -tfirst, then reload. - Trusting everyone in the application: setting the trusted proxy list to "all addresses" means forged headers are accepted too.
Verifying the setup
- From inside the server, try the application directly: if it answers, the backend is up.
- Try the same request through Nginx using the domain name: if the answer is the same, the proxy works.
- From outside the server, confirm that
http://server-ip:3000does not open. - Check in the application's log that the visitor IP is the real address, not
127.0.0.1. - If something is wrong, look at the Nginx error log; in most installations it is
/var/log/nginx/error.log.
# From inside the server: does the application answer directly?
curl -I http://127.0.0.1:3000/
# Through Nginx: does the domain name give the same answer?
curl -I http://example.com/
# If there is an error, the last log lines (this is the path in most installations)
sudo tail -n 50 /var/log/nginx/error.logFrequently asked questions
Should I put a trailing slash on proxy_pass?
It depends on the path your application expects. With a slash (or any other path), Nginx replaces the prefix matched by the location with that path; without one, it passes the address on unchanged. In the plain case where you forward the whole site with "location /", both give the same result.
Why does my application see every visitor as 127.0.0.1?
Because the connection is made by Nginx, not by the visitor. Forward the X-Real-IP and X-Forwarded-For headers in Nginx and define your reverse proxy's address as a trusted proxy in the application.
What is the difference between reload and restart?
A reload applies the configuration without dropping open connections; if the configuration is invalid, Nginx keeps running with the old settings. A restart stops and starts Nginx; with an invalid configuration it may not start at all. For day-to-day changes a reload is enough.
What changes if the application is on another server?
You write that server's internal network address in proxy_pass (e.g. http://10.0.0.5:3000). Bind the application to the internal address, open the port in the firewall to the Nginx server only, and add the Nginx server's internal address to the application's trusted proxy list.
Should the traffic between Nginx and the application be encrypted too?
If both are on the same server, the traffic never leaves it; terminating HTTPS at Nginx (SSL/TLS termination) is a common and sufficient arrangement. If the traffic crosses a network you do not trust, consider encrypting the backend connection as well.
BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.
Related guides
- What is a reverse proxy?What a reverse proxy does, typical architectures, the real visitor IP and headers, how it relates to a CDN, and a short Nginx example.
- HTTPS in Nginx: setting up an SSL certificateGetting a certificate, redirecting to HTTPS, HSTS, SSL termination and automatic renewal in Nginx.
- 502, 504 and other Nginx errors: causes and fixesWhat 502, 504, 413, 499, 403 and 404 mean, their likely causes, diagnosis from the logs and the fix.
- What is load balancing?How requests are spread across several servers, the Nginx upstream settings, health checks and the session problem.
Let us talk about your website infrastructure
BYK Yazılım builds corporate websites. Write to us with any questions about your site.
Contact us Our corporate website service