🌐 Networking Tutorials How to Set Up an Nginx Reverse Proxy

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.