Why You Need a Reverse Proxy
So you’ve got a server. Maybe a few — one running a blog, another running a dashboard, maybe a home automation interface hiding on port 8123. Right now, you’re typing IP addresses and port numbers like it’s 1998. There’s a better way.
A reverse proxy sits at the front door of your infrastructure and routes traffic to the right internal service based on the domain name. Instead of 192.168.1.50:3000 for Grafana and 192.168.1.50:8080 for your wiki, you get clean URLs like grafana.yourdomain.com and wiki.yourdomain.com — with proper HTTPS, automatically. No more memorizing port numbers. No more self-signed certificate warnings.
Nginx is the gold standard for this. It’s lightweight, battle-tested, and when paired with Certbot, gives you completely free, automatically-renewing SSL certificates from Let’s Encrypt. Let’s set it up.
Prerequisites
- A Linux server (Ubuntu 22.04 or Debian 12 recommended) with a public IP address
- A domain name (or several) pointed at your server’s IP
- SSH access with sudo privileges
- Ports 80 and 443 open in your firewall
This guide uses Ubuntu 22.04 LTS, but the steps are nearly identical on Debian and most systemd-based distros.
Step 1: Install Nginx
sudo apt update
sudo apt install nginx -y
Verify it’s running:
sudo systemctl status nginx
You should see active (running) in green. You can also visit your server’s IP in a browser — you should see the default Nginx welcome page.
Step 2: Set Up DNS Records
For each service you want to proxy, create an A record in your DNS provider pointing to your server’s IP. For example:
| Record | Type | Value |
|---|---|---|
| grafana.yourdomain.com | A | 203.0.113.50 |
| wiki.yourdomain.com | A | 203.0.113.50 |
Tip: If you’re working in a homelab without a public domain, you can use a service like DuckDNS or edit /etc/hosts for local testing. But Let’s Encrypt needs a real, publicly accessible domain to issue certificates, so for production use, you need real DNS.
Wait a few minutes for DNS to propagate, then verify:
dig +short grafana.yourdomain.com
It should return your server’s IP.
Step 3: Create Nginx Configuration for Each Site
Nginx stores site configurations in /etc/nginx/sites-available/ and symlinks them from /etc/nginx/sites-enabled/. Let’s create our first one:
sudo nano /etc/nginx/sites-available/grafana.yourdomain.com
Paste this configuration:
server {
listen 80;
server_name grafana.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 does, because understanding this template means you can adapt it for anything:
listen 80— Accept HTTP traffic initially (we’ll add HTTPS in a moment)server_name— Nginx will only respond to requests for this domainproxy_pass— Forward traffic to your service running on port 3000proxy_set_header Host— Tell the backend service what hostname the visitor requestedproxy_set_header X-Real-IP— Pass the visitor’s real IP address to the backendproxy_set_header X-Forwarded-Proto— Tell the backend whether the original request was HTTP or HTTPSUpgradeandConnectionheaders — Required for WebSocket support (Grafana uses WebSockets for live dashboards)
Enable the site by creating a symlink:
sudo ln -s /etc/nginx/sites-available/grafana.yourdomain.com /etc/nginx/sites-enabled/
Test the configuration for syntax errors:
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
Reload Nginx to apply:
sudo systemctl reload nginx
At this point, visiting http://grafana.yourdomain.com (note: HTTP, not HTTPS yet) should show your Grafana dashboard. If it does — great! The proxy works. Now let’s add encryption.
Step 4: Install Certbot and Get SSL Certificates
Certbot automates the entire SSL certificate lifecycle — issuance, installation, and renewal. Install it:
sudo apt install certbot python3-certbot-nginx -y
This installs both Certbot and the Nginx plugin, which can automatically reconfigure Nginx to use your certificates. Now request a certificate:
sudo certbot --nginx -d grafana.yourdomain.com
Certbot will:
- Validate you control the domain (via an HTTP-01 challenge on port 80)
- Download and install the SSL certificate
- Automatically modify your Nginx config to redirect HTTP to HTTPS
- Set up the TLS configuration with modern, secure defaults
It will ask for an email address (for renewal notices) and whether to redirect HTTP to HTTPS — say yes to the redirect.
When it’s done, your Nginx config will look something like this:
server {
listen 80;
server_name grafana.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name grafana.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/grafana.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grafana.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 what changed: the HTTP server block now returns a 301 redirect, and a new HTTPS server block handles encrypted traffic with the Let’s Encrypt certificate paths. Your service is now accessible at https://grafana.yourdomain.com with a valid, trusted SSL certificate.
Step 5: Verify Auto-Renewal
Let’s Encrypt certificates expire every 90 days, but Certbot sets up automatic renewal via a systemd timer. Verify it’s active:
sudo systemctl status certbot.timer
You should see it listed as active. You can do a dry run to make sure renewal works:
sudo certbot renew --dry-run
If you see “Congratulations, all renewals succeeded,” you’re golden. Certbot will handle renewals automatically from now on, and the Nginx plugin will reload the web server to pick up new certificates.
Step 6: Add More Services
Now that you have the pattern down, adding more services is straightforward. For each new service:
- Create an A record in DNS
- Create an Nginx config in
/etc/nginx/sites-available/(copy your working one and changeserver_nameandproxy_pass) - Symlink it to sites-enabled
- Run
certbot --nginx -d newdomain.com
For example, a second service might look like:
server {
listen 80;
server_name wiki.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
Then sudo certbot --nginx -d wiki.yourdomain.com and you’re done.
Common Pitfalls
“502 Bad Gateway”
This means Nginx can’t reach your backend service. Check that the service is actually running on the port you specified, and that it’s listening on 127.0.0.1 (not just a Unix socket). Verify with:
sudo ss -tlnp | grep :3000
Certbot fails the challenge
If domain validation fails, make sure:
- Your DNS A record is correct and has propagated (
dig +short yourdomain.com) - Nothing else is blocking port 80 (temporarily stop other web servers)
- You can reach port 80 from outside your network
SSL certificate warnings in the browser
Don’t try to proxy HTTPS-to-HTTPS unnecessarily. Your backend services should listen on HTTP locally — Nginx handles encryption on the public side. If your backend only supports HTTPS with a self-signed cert, add proxy_ssl_verify off; in the location block, but this is a workaround, not a best practice.
WebSocket connections don’t work
If your application uses WebSockets (Grafana, Home Assistant, Jupyter), you must include the Upgrade and Connection headers shown in the config above. Without them, real-time features will silently fail.
Security Hardening Bonus
Let’s Encrypt gives you an A+ rating on SSL Labs by default, but you can add a few extra headers. Create a file /etc/nginx/conf.d/security.conf:
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Hide Nginx version
server_tokens off;
Reload Nginx and these headers will apply to all your proxied sites. It’s a small thing, but it closes common attack vectors and shows you care about the details.
Wrapping Up
That’s it. You now have a production-ready reverse proxy setup with free, automatically-renewing SSL certificates. Every service on your network gets a clean URL and a padlock icon in the browser.
The beauty of this pattern is its composability: this Nginx setup pairs perfectly with the other guides in this series. Your Pi-hole gets pihole.yourdomain.com. Your monitoring stack gets grafana.yourdomain.com and prometheus.yourdomain.com. Your VPN configuration is already encrypted at the protocol level. You’re building a real infrastructure, one clean URL at a time.
Go forth and proxy all the things.
Leave a Reply