Nginx Reverse Proxy Configuration: Put Node.js, Python or Java Apps Behind Nginx
To put an app behind nginx as a reverse proxy, run the app on a private port (for example 127.0.0.1:3000) and add a server block with proxy_pass http://127.0.0.1:3000; plus the Host and X-Forwarded-* headers. Test with sudo nginx -t and reload. On Amazon Linux, RHEL or Rocky, also run sudo setsebool -P httpd_can_network_connect 1, or SELinux blocks nginx and you see 502 Bad Gateway. Add HTTPS with certbot.
Quick answer: /etc/nginx/conf.d/app.conf
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
A live example first
We run a Node.js (Express) API on an Amazon Linux 2023 EC2 instance, on port 3000. Users should reach it at api.example.com on port 80/443, and port 3000 should stay closed in the security group.
- Check the app:
curl -s 127.0.0.1:3000/health
{"ok":true}
- Create
/etc/nginx/conf.d/api.confwith the quick-answer block (server_nameapi.example.com). - Test and reload:
sudo nginx -t && sudo systemctl reload nginx
curl -I http://api.example.com/health
HTTP/1.1 502 Bad Gateway
- Read the error log:
sudo tail -n 3 /var/log/nginx/error.log
connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstream
Permission denied (13) while the app is running means SELinux. Fix:
sudo setsebool -P httpd_can_network_connect 1
curl -I http://api.example.com/health
HTTP/1.1 200 OK
- Add HTTPS:
sudo certbot --nginx -d api.example.com. Friends, the app never touched the internet directly.
How does a reverse proxy work?
Users talk only to nginx; it forwards to your app on a private port and handles HTTPS.
nginx accepts the user's connection, handles TLS, then opens its own connection to the app and copies the request across, adding headers so the app knows the real client IP and scheme. The reply comes back the same way. This lets nginx do HTTPS, compression, caching, rate limits and static files, while the app does business logic.
How do I set it up step by step?
- Make the app listen on
127.0.0.1:PORT(not0.0.0.0). - Create a file in
/etc/nginx/conf.d/(Amazon Linux/RHEL) or/etc/nginx/sites-available/+ symlink (Ubuntu). - Add
server_name,location /andproxy_pass. - Add the four
proxy_set_headerlines. - Run
sudo nginx -t. - Run
sudo systemctl reload nginx. - On SELinux systems, set
httpd_can_network_connect. - Open only 80/443 in the security group; keep the app port closed.
- Add HTTPS with certbot.
Per-stack examples
| App | Typical upstream | Notes |
|---|---|---|
| Node.js (Express, Next.js) | proxy_pass http://127.0.0.1:3000; |
Run with PM2 or systemd; set app.set('trust proxy', 1) |
| Python (Gunicorn / Uvicorn) | proxy_pass http://unix:/run/gunicorn.sock; or 127.0.0.1:8000 |
Django: SECURE_PROXY_SSL_HEADER; FastAPI: --proxy-headers |
| Java (Spring Boot, Tomcat) | proxy_pass http://127.0.0.1:8080; |
Spring: server.forward-headers-strategy=native |
| PHP (PHP-FPM) | fastcgi_pass unix:/run/php-fpm/www.sock; |
FastCGI, not proxy_pass |
| .NET (Kestrel) | proxy_pass http://127.0.0.1:5000; |
Use UseForwardedHeaders() |
| Go | proxy_pass http://127.0.0.1:8080; |
Read X-Forwarded-For yourself |
Useful extras
# WebSockets (Socket.IO, live dashboards)
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Bigger uploads and slow reports
client_max_body_size 20m;
proxy_read_timeout 120s;
# Two app servers (load balancing)
upstream api_pool { server 10.0.1.10:3000; server 10.0.2.10:3000; }
# then: proxy_pass http://api_pool;
Trailing slash rule: location /api/ { proxy_pass http://127.0.0.1:3000/; } sends /api/users to the app as /users. Without the final / on proxy_pass, the app gets /api/users.
502, 504 and 413 errors
| Error | Common cause | Fix |
|---|---|---|
| 502 Bad Gateway | App down / wrong port | curl 127.0.0.1:PORT, check systemctl status app |
| 502 + "Permission denied (13)" | SELinux | setsebool -P httpd_can_network_connect 1 |
| 504 Gateway Timeout | App too slow | Speed up app, or raise proxy_read_timeout |
| 413 Request Entity Too Large | Upload over 1 MB default | client_max_body_size 20m; |
| Redirect loop to http | App does not see HTTPS | Send X-Forwarded-Proto and trust it in the app |
Real example
In February 2017, Cloudflare disclosed "Cloudbleed": a bug in an HTML parser running inside their edge proxy (built on nginx) leaked bits of memory, including some private data, into responses for months. It reminds us that the proxy sees every request and must be kept updated. Run sudo dnf update nginx (or apt) regularly, and keep only the modules you need.
Mistakes to avoid
- Leaving the app port open in the security group. Only nginx should be public.
- Turning SELinux off to fix a 502. Set the boolean instead.
- Forgetting
nginx -tbefore reload.
Summary table
| Item | Setting |
|---|---|
| Forward | proxy_pass http://127.0.0.1:PORT; |
| Headers | Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto |
| WebSockets | proxy_http_version 1.1 + Upgrade/Connection |
| Uploads | client_max_body_size |
| SELinux | httpd_can_network_connect 1 |
| HTTPS | certbot --nginx |
Quick quiz: check what you learned
What does proxy_pass set?
- Where nginx forwards requests
- The nginx worker count
- The log file location
- The TLS certificate file
Show answer
A. proxy_pass names the upstream app.
nginx returns 502 and SELinux is on. Common fix?
- httpd_can_network_connect on
- Restart the EC2 instance
- Delete the nginx logs
- Open port 22 to all
Show answer
A. SELinux blocks nginx outbound connects until this boolean is set.
Which header carries the original scheme?
- X-Forwarded-Proto
- X-Content-Type
- Cache-Control
- Accept-Language
Show answer
A. Apps use it to know the user came over HTTPS.
What do WebSockets need in nginx?
- Upgrade and Connection headers
- A bigger client body size
- A new server_name value
- Gzip turned off fully
Show answer
A. WebSockets switch protocols using these headers.
Learn it properly
These pages go deeper, with labs:
Frequently asked questions
What is a reverse proxy?
A server that receives requests from users and forwards them to an app behind it. Users only see nginx; the app stays on a private port.
What does proxy_pass do?
It tells nginx where to send the request, for example proxy_pass http://127.0.0.1:3000;.
Why do I get 502 Bad Gateway?
nginx could not get a response from the app: it is not running, listens on another port, or SELinux blocks nginx from connecting.
How do I pass the real client IP?
Set X-Real-IP and X-Forwarded-For headers, and configure the app to trust them from nginx.
How do I proxy WebSockets?
Use proxy_http_version 1.1 and set the Upgrade and Connection headers.
Does a trailing slash in proxy_pass matter?
Yes. With a URI part such as http://app:3000/, nginx replaces the matched location prefix; without it, the full original path is passed.