How to Set Up Nginx as a Reverse Proxy with Free SSL from Let’s Encrypt

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 domain
  • proxy_pass — Forward traffic to your service running on port 3000
  • proxy_set_header Host — Tell the backend service what hostname the visitor requested
  • proxy_set_header X-Real-IP — Pass the visitor’s real IP address to the backend
  • proxy_set_header X-Forwarded-Proto — Tell the backend whether the original request was HTTP or HTTPS
  • Upgrade and Connection headers — 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:

  1. Validate you control the domain (via an HTTP-01 challenge on port 80)
  2. Download and install the SSL certificate
  3. Automatically modify your Nginx config to redirect HTTP to HTTPS
  4. 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:

  1. Create an A record in DNS
  2. Create an Nginx config in /etc/nginx/sites-available/ (copy your working one and change server_name and proxy_pass)
  3. Symlink it to sites-enabled
  4. 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

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