How to Set Up an Nginx Reverse Proxy
Configure Nginx as a reverse proxy to forward HTTP and HTTPS traffic to a backend app, with SSL termination and real IP forwarding.
A reverse proxy sits in front of your application and forwards incoming requests to it. Nginx handles SSL termination, compression, and connection limits so your app does not have to. This tutorial assumes your app is already running locally (e.g., a Node, Python, or Go server on port 3000).
Install Nginx
sudo apt update && sudo apt install nginx # Debian / Ubuntu
sudo dnf install nginx # RHEL / Fedora / Rocky
sudo systemctl enable --now nginx
Basic reverse proxy configuration
Create a new site config at /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name myapp.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Enable it and reload:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx -t validates the config before you reload. Always run it first — a syntax error in the config takes Nginx down on reload.
The Upgrade and Connection headers are needed for WebSocket support. They are harmless for regular HTTP.
Add HTTPS with Certbot
Install Certbot and the Nginx plugin:
sudo apt install certbot python3-certbot-nginx # Debian / Ubuntu
sudo dnf install certbot python3-certbot-nginx # RHEL / Fedora
Issue a certificate and let Certbot rewrite your config:
sudo certbot --nginx -d myapp.example.com
Certbot rewrites the server block to listen on 443 with SSL and adds a redirect from 80 to 443. Your working config after Certbot looks like this:
server {
listen 443 ssl;
server_name myapp.example.com;
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80;
server_name myapp.example.com;
return 301 https://$host$request_uri;
}
Certbot installs a systemd timer that renews certificates automatically. Verify it:
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
Proxy timeouts
The Nginx defaults (60 seconds) are too short for long-running requests (file uploads, AI inference, etc.). Raise them per-location:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 120s;
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
}
proxy_connect_timeout is how long Nginx waits to establish the connection to the backend. proxy_read_timeout is how long it waits between successive reads from the backend — not the total response time.
Buffer tuning
By default Nginx buffers responses from the backend before sending them to the client. For most apps this is fine. For streaming responses (server-sent events, chunked transfer), disable buffering:
location /stream {
proxy_pass http://127.0.0.1:3000;
proxy_buffering off;
proxy_cache off;
}
Proxy multiple apps on one server
Add a separate server block for each domain, each pointing to a different backend port:
server {
listen 443 ssl;
server_name api.example.com;
# ssl config ...
location / {
proxy_pass http://127.0.0.1:3001;
}
}
server {
listen 443 ssl;
server_name dashboard.example.com;
# ssl config ...
location / {
proxy_pass http://127.0.0.1:3002;
}
}
Each virtual host is independent. One Nginx process handles all of them.
Check that it is working
curl -I https://myapp.example.com
You should see HTTP/2 200 (or 301 on the HTTP version). The X-Forwarded-For header confirms Nginx is passing the client IP through.
Check Nginx logs if something is wrong:
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
For an open-port audit to confirm only 80 and 443 are externally reachable, see the Audit Open Ports tutorial. To add rate limiting and request throttling in front of your app, combine this setup with the firewall rules in Harden a Linux Server.
SysEmperor