How to Set Up an Nginx Reverse Proxy: The Single Gateway for All Your Self-Hosted Services

Why You Need a Reverse Proxy

Picture this: you’ve got a dozen services running on your home server — a media dashboard, a file sync tool, a personal wiki, a monitoring panel. Right now, each one lives at http://192.168.1.50:8080, http://192.168.1.50:3000, http://192.168.1.50:9090 … the list grows, and your bookmarks become a mess of port numbers.

A reverse proxy fixes this. It sits at the front door of your network, listens on standard ports (80 and 443), and routes traffic to the right backend service based on the domain name or path. You type dashboard.home.arpa and it just works. No ports to remember. Clean URLs. Centralized SSL termination. One place to manage access.

Nginx is the gold standard for this. It’s lightweight, battle-tested, and its configuration language — while initially cryptic — is incredibly powerful once you understand the basics.

What You’ll Need

  • A Linux server (Ubuntu 22.04+ or Debian 12+ recommended)
  • Root or sudo access
  • At least one backend service already running (we’ll use a simple example)
  • A domain name (or a local DNS setup like Pi-hole or /etc/hosts entries)
  • Basic comfort with the command line

Step 1: Install Nginx

On Ubuntu or Debian, installation is straightforward:

sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx

Verify it’s running by visiting your server’s IP in a browser. You should see the default Nginx welcome page. That means Nginx is alive and listening on port 80.

Step 2: Understand the Configuration Structure

Before we dive in, let’s map out how Nginx organizes its config. This saves a lot of confusion later:

  • /etc/nginx/nginx.conf — The main config file. Global settings, worker processes, events. You rarely need to touch this.
  • /etc/nginx/sites-available/ — Where you write your server block configurations. These are potential configs, not yet active.
  • /etc/nginx/sites-enabled/ — Symlinks to the configs you want active. Nginx reads from here.
  • /etc/nginx/snippets/ — Reusable config fragments (great for shared SSL settings).

This sites-availablesites-enabled pattern is your best friend. It means you can write a config, test it, and only activate it when you’re ready. No more editing a live config and accidentally taking down your entire web server with a typo.

Step 3: Create Your First Reverse Proxy

Let’s say you have a service running on port 3000 (maybe it’s Uptime Kuma, or a personal dashboard). You want it accessible at status.yourdomain.com.

Create a new server block:

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

Add the following configuration:

server {
    listen 80;
    server_name status.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_cache_bypass $http_upgrade;
    }
}

Let’s break down what each line actually does, because understanding this template will let you adapt it to any service:

  • listen 80 — Listen for HTTP traffic on port 80. We’ll add HTTPS later.
  • server_name — The domain name that triggers this block. If someone requests this domain, Nginx routes the request here.
  • proxy_pass — The actual forwarding. “Take this request and send it to this address.” This is the heart of the reverse proxy.
  • proxy_http_version 1.1 — Use HTTP/1.1 for the connection to the backend. Required for WebSocket support.
  • Upgrade and Connection headers — These enable WebSocket proxying. Many modern dashboards use WebSockets for real-time updates. Without these, features like live notifications break silently.
  • proxy_set_header Host $host — Pass the original hostname to the backend. Without this, your backend might see requests as coming from 127.0.0.1 and generate broken links.
  • X-Real-IP and X-Forwarded-For — Pass the visitor’s real IP address to the backend. Without these, your backend logs will show every request as coming from 127.0.0.1, which makes debugging and analytics useless.
  • X-Forwarded-Proto — Tell the backend whether the original request was HTTP or HTTPS. Some apps use this to generate correct redirect URLs.

Step 4: Enable the Site and Test

Create the symlink to activate your config:

sudo ln -s /etc/nginx/sites-available/status /etc/nginx/sites-enabled/

Always test your configuration before reloading. This single habit has saved me countless times:

sudo nginx -t

You should see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If you get an error, check the line number mentioned. Nine times out of ten, it’s a missing semicolon or a typo in a directive name. Now reload:

sudo systemctl reload nginx

reload (not restart) gracefully picks up config changes without dropping active connections. Use it. Love it.

Step 5: Add HTTPS with Let’s Encrypt

HTTP-only is fine for local networks, but if your reverse proxy is exposed to the internet, you need HTTPS. Certbot makes this almost embarrassingly easy:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d status.yourdomain.com

Certbot will:

  1. Verify you control the domain
  2. Obtain a certificate from Let’s Encrypt
  3. Automatically modify your Nginx config to use HTTPS
  4. Set up automatic renewal (certificates expire every 90 days)

After Certbot runs, your config will look something like this (the comments are Certbot’s additions):

server {
    listen 80;
    server_name status.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name status.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/status.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/status.yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_cache_bypass $http_upgrade;
    }
}

Notice the HTTP server block now just redirects to HTTPS. This is the correct pattern — always redirect, never serve content over both.

Step 6: Scale It — Multiple Services, One Proxy

This is where the reverse proxy truly shines. Adding a second service is just creating another server block. Let’s add a wiki at wiki.yourdomain.com running on port 8080:

sudo nano /etc/nginx/sites-available/wiki
server {
    listen 443 ssl;
    server_name wiki.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/wiki.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/wiki.yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_cache_bypass $http_upgrade;
    }
}

server {
    listen 80;
    server_name wiki.yourdomain.com;
    return 301 https://$host$request_uri;
}

Enable, test, reload. Done. You now have two services, each with its own domain and HTTPS certificate, both served through a single Nginx instance.

Common Pitfalls (and How to Avoid Them)

1. The Dreaded 502 Bad Gateway

This means Nginx can’t reach your backend. Check that the service is actually running on the port you specified. curl http://127.0.0.1:3000 from the server itself will tell you if the backend is alive. If it works locally but Nginx still shows 502, check for firewall rules or that the service is binding to 127.0.0.1 (not just the external interface).

2. WebSocket Connections Breaking

If your app has real-time features that stop working behind the proxy, you almost certainly forgot the Upgrade and Connection headers. Add them. Also, some apps need proxy_read_timeout 86400s; in the location block to keep long-lived WebSocket connections alive.

3. Mixed Content Warnings

If your backend generates links using http:// instead of https://, browsers will block resources. The X-Forwarded-Proto header helps, but some apps need additional configuration to respect it. Check your app’s docs for a “trusted proxies” or “behind reverse proxy” setting.

4. Forgetting to Remove the Default Site

The default Nginx welcome page lives in /etc/nginx/sites-enabled/default. If you forget to remove it, requests to your server’s IP (without a matching server_name) will show the default page instead of a proper error. Remove it: sudo rm /etc/nginx/sites-enabled/default.

5. Certbot Failing to Renew

Automatic renewal relies on a cron job or systemd timer. Verify it exists: sudo systemctl list-timers | grep certbot. If renewals fail silently, your certificate expires and visitors see scary browser warnings. Set up a monitoring check — this is exactly the kind of thing your reverse proxy should be serving.

Going Further

Once you’re comfortable with the basics, here are some natural next steps:

  • Basic Authentication: Add a login prompt to services that don’t have their own auth. Use auth_basic and auth_basic_user_file directives.
  • Rate Limiting: Protect login endpoints from brute force with limit_req_zone.
  • Access Lists: Restrict certain paths to specific IP ranges with allow and deny directives.
  • Fail2Ban Integration: Monitor Nginx logs for suspicious patterns and automatically block offending IPs.
  • Wildcard Certificates: If you have many subdomains, a wildcard cert from Certbot (*.yourdomain.com) saves you from managing individual certificates.

Wrapping Up

A reverse proxy is one of those infrastructure pieces that quietly makes everything better. Once it’s set up, you stop thinking about ports and start thinking about services. Each new thing you self-host gets a clean URL, automatic HTTPS, and a single point for security policies.

The initial configuration might feel like you’re learning a new language. But the template above — the proxy_pass block with all the headers — is genuinely all you need for 90% of use cases. Copy it, adapt it, and move on to the fun part: actually running the services.

Your future self, bookmarking home.yourdomain.com instead of trying to remember whether the dashboard is on port 8080 or 8081, will thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *