What you will build

A reverse proxy sits in front of your application and forwards client requests to it. In this guide you will set up Nginx as a reverse proxy on Ubuntu 24.04, forwarding traffic from port 80 to a backend application running on port 3000. You will learn the essential proxy_pass configuration, the headers every reverse proxy needs, how to verify everything works, and how to fix the most common errors. By the end, visitors will reach your app through Nginx while your backend stays safely hidden behind it.

This is for anyone running a Node.js, Python, or any other app on a VPS or local server who wants a clean public URL, SSL termination, and a single entry point in front of one or more backends.

Prerequisites

  • An Ubuntu 24.04 (or 22.04) server with a non-root user that has sudo privileges.
  • A backend application listening on some local port. No app yet? Step 2 creates a tiny demo one so you can follow along exactly.
  • A domain name pointing at your server is helpful but not required; localhost or the server IP works for testing.
  • A few minutes and a terminal.

Step 1: Install Nginx

Update the package index and install Nginx from the official Ubuntu repositories:

sudo apt update
sudo apt install -y nginx

apt update refreshes the list of available packages so you get the latest stable Nginx for your Ubuntu release. The -y flag answers "yes" to the install confirmation automatically.

Confirm the installation and check that Nginx started:

nginx -v
sudo systemctl status nginx --no-pager

nginx -v prints the version. The systemctl status output should show active (running). If Nginx is not running, start it with sudo systemctl start nginx.

Step 2: Create a demo backend app (skip if you already have one)

To test the reverse proxy you need something listening on a port. This one-liner starts Python's built-in HTTP server on port 3000:

nohup python3 -m http.server 3000 > /tmp/demo-app.log 2>&1 &
curl http://127.0.0.1:3000 -I

nohup ... & keeps the server running after you close the terminal, and the output goes to /tmp/demo-app.log. The curl -I request should return HTTP/1.0 200 OK, which proves your backend is alive on port 3000. If you already run a Node.js or other app, note its port and use that instead of 3000 in the steps below.

Step 3: Create the reverse proxy site configuration

Nginx reads site configs from /etc/nginx/sites-available/ and serves the ones linked into /etc/nginx/sites-enabled/. Create a new config file for your app:

sudo nano /etc/nginx/sites-available/myapp

Paste in this configuration:

server {
    listen 80;
    server_name example.com www.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";
    }
}

Replace example.com with your real domain, or with _ (catch-all) if you are testing by IP. Replace 3000 with your backend's port.

Step 4: Understand what each directive does

Do not copy-paste blindly; here is what the important lines mean:

  • proxy_pass http://127.0.0.1:3000; — the heart of the reverse proxy. Every request matching location / is forwarded to the backend address. Note the trailing ;: Nginx config will not load without it.
  • proxy_set_header Host $host; — passes the original hostname to your app. Without this, the backend sees 127.0.0.1 as the host, which breaks apps that generate URLs or check the Host header.
  • X-Real-IP and X-Forwarded-For — forward the visitor's real IP address. Otherwise your app's logs will show every visitor as 127.0.0.1.
  • X-Forwarded-Proto $scheme; — tells the app whether the original request was http or https, so it can build correct links and avoid redirect loops behind HTTPS.
  • Upgrade / Connection — allow WebSocket connections to pass through. Harmless for plain HTTP apps, essential for chat apps and live dashboards.
  • proxy_http_version 1.1; — required for keep-alive connections and WebSockets to work correctly through the proxy.

Step 5: Enable the site and reload Nginx

Link the config into sites-enabled, test the syntax, then reload:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t checks your configuration for syntax errors before anything goes live. It should print syntax is ok and test is successful. If it reports an error, fix it and re-run nginx -t before reloading. systemctl reload applies the new config with zero downtime; never use restart for a config change.

If you are testing by IP address with no domain, also remove the default site so it does not intercept your requests: sudo rm /etc/nginx/sites-enabled/default, then reload again.

Step 6: Open the firewall

If UFW is enabled, allow web traffic:

sudo ufw allow 'Nginx Full'
sudo ufw status

'Nginx Full' opens both port 80 (HTTP) and port 443 (HTTPS). If you only want plain HTTP for now, use 'Nginx HTTP' instead.

Step 7: Verify it worked

Run these checks from your server and from a browser:

curl -I http://localhost
curl http://localhost | head -20

The first command should return HTTP/1.1 200 OK with a Server: nginx header. The second should show the content served by your backend app (for the Python demo server, a directory listing). If you see your app's content coming through port 80, the reverse proxy is working: the client talks to Nginx, Nginx talks to your app.

Now open http://your-domain-or-ip in a browser. You should see your application, not the default Nginx welcome page. Check your backend's logs: with the X-Forwarded-For headers in place, requests should carry the real client IP.

Step 8 (recommended): Add free HTTPS with Certbot

A reverse proxy is the natural place to terminate SSL: Nginx handles encryption, and the backend keeps speaking plain HTTP on localhost. Install Certbot and get a certificate:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot will ask for an email, agree to the terms, and offer to redirect all HTTP traffic to HTTPS; choose the redirect option. It automatically edits your Nginx config, adding a listen 443 ssl; block with the certificate paths. Verify the renewal timer is active:

sudo systemctl status certbot.timer --no-pager

From now on, visitors connect to Nginx over HTTPS, and Nginx forwards the decrypted request to your app on 127.0.0.1:3000. Your backend never touches TLS certificates.

Step 9 (optional): Load-balance across two backends

One of the biggest wins of a reverse proxy is spreading traffic. Define an upstream group and point proxy_pass at it:

upstream backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend;
        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;
    }
}

Nginx distributes requests round-robin across the two servers by default, and automatically skips one that stops responding. Test and reload with sudo nginx -t && sudo systemctl reload nginx.

Common issues and fixes

1. "502 Bad Gateway"

This is the classic reverse proxy error and it almost always means Nginx is fine but the backend is not reachable. Check the backend directly:

curl -v http://127.0.0.1:3000

If the connection is refused, your app is not running or is listening on a different port. Start it, then confirm the port matches the proxy_pass address exactly. Also check SELinux or a host firewall is not blocking localhost connections.

2. nginx -t fails with "[emerg]"

Read the error line carefully; it names the file and line number. The usual suspects are a missing semicolon, mismatched braces, or two server blocks both declaring default_server. Fix it, re-run nginx -t, and only then reload.

3. The default Nginx welcome page still shows

Your new site is not the one answering. Either the server_name does not match the domain you are visiting, or the old default site is still enabled and catching the request. Remove it with sudo rm /etc/nginx/sites-enabled/default and reload.

4. Your app logs every visitor as 127.0.0.1

The proxy headers are missing. Make sure your location block includes the X-Real-IP and X-Forwarded-For lines from Step 3, and that your application is configured to trust the proxy's forwarded headers (in Express, for example, app.set('trust proxy', true)).

5. WebSocket connections drop immediately

Add the Upgrade and Connection headers from the Step 3 config. Without them, Nginx treats the WebSocket handshake as a normal HTTP request and the socket never upgrades.

Conclusion

You now have Nginx running as a reverse proxy on Ubuntu: it accepts public traffic on port 80 (and 443 with Certbot), forwards it to your backend with the correct headers, and can load-balance across multiple app instances. This single pattern covers most real-world deployments, from a side project on a VPS to a production API fleet.

If server setups like this eat up your team's time, XpertBees handles Nginx configuration, SSL, hardening, and full server management for businesses so your developers can stay focused on the product.