Ravindra BagaleCourses & study guides Track your progress

Guides

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.

  1. Check the app:
curl -s 127.0.0.1:3000/health
{"ok":true}
  1. Create /etc/nginx/conf.d/api.conf with the quick-answer block (server_name api.example.com).
  2. Test and reload:
sudo nginx -t && sudo systemctl reload nginx
curl -I http://api.example.com/health
HTTP/1.1 502 Bad Gateway
  1. 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
  1. Add HTTPS: sudo certbot --nginx -d api.example.com. Friends, the app never touched the internet directly.

How does a reverse proxy work?

nginx reverse proxy Users connect to nginx on 443, nginx adds headers and forwards to the app on a private port, then returns the reply. nginx reverse proxy UserHTTPS 443nginxTLS + headersproxy_pass127.0.0.1:3000AppNode / JavaReplyback to user you Only nginx faces the internet. The app listens on localhost

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?

  1. Make the app listen on 127.0.0.1:PORT (not 0.0.0.0).
  2. Create a file in /etc/nginx/conf.d/ (Amazon Linux/RHEL) or /etc/nginx/sites-available/ + symlink (Ubuntu).
  3. Add server_name, location / and proxy_pass.
  4. Add the four proxy_set_header lines.
  5. Run sudo nginx -t.
  6. Run sudo systemctl reload nginx.
  7. On SELinux systems, set httpd_can_network_connect.
  8. Open only 80/443 in the security group; keep the app port closed.
  9. 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

  1. Leaving the app port open in the security group. Only nginx should be public.
  2. Turning SELinux off to fix a 502. Set the boolean instead.
  3. Forgetting nginx -t before 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

  1. What does proxy_pass set?

    1. Where nginx forwards requests
    2. The nginx worker count
    3. The log file location
    4. The TLS certificate file
    Show answer

    A. proxy_pass names the upstream app.

  2. nginx returns 502 and SELinux is on. Common fix?

    1. httpd_can_network_connect on
    2. Restart the EC2 instance
    3. Delete the nginx logs
    4. Open port 22 to all
    Show answer

    A. SELinux blocks nginx outbound connects until this boolean is set.

  3. Which header carries the original scheme?

    1. X-Forwarded-Proto
    2. X-Content-Type
    3. Cache-Control
    4. Accept-Language
    Show answer

    A. Apps use it to know the user came over HTTPS.

  4. What do WebSockets need in nginx?

    1. Upgrade and Connection headers
    2. A bigger client body size
    3. A new server_name value
    4. Gzip turned off fully
    Show answer

    A. WebSockets switch protocols using these headers.

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.