Previous post

Reverse Proxy for the Homelab

Last week, I finally got around to set up an nginx reverse proxy for my home lab. Because it turned out a bit more involved than I had anticipated, I decided to share my notes.

This has been a long-term item on the todo list. The main motivation being that it is to hard to remember all the ports I have configured on various devices, and that password-managers tend to suggest multiple logins (when ports are the only thing separating the urls).

An added requirement was that I wanted to get this working at home but also when vpn-ing in via tailscale.

Nginx via Docker

The first step was to create an nginx container. I used the following docker compose:

# docker compose

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./snippets:/etc/nginx/snippets
    ports:
      - 80:80
      - 443:443

The nginx.conf looks roughly like this

# nginx config

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile           on;
    keepalive_timeout  65;

    server {
        listen 80;
        server_name portainer.tiny.pnet;
        include /etc/nginx/snippets/proxy-defaults.conf;
        location / {
            proxy_pass http://192.168.0.42:9000;
        }
    }
	server {
        listen 80;
        server_name plex.tiny.pnet;
        include /etc/nginx/snippets/proxy-defaults.conf;
        location / {
            proxy_pass http://192.168.0.42:32400;
        }
    }

    server {
        listen 80 default_server;
        server_name _;

        return 404;
    }
}

In the example, I want to reach my main home server as tiny. It runs on 192.168.0.42 and here I only created routes for the portainer and plex frontends. More can be added using the same pattern. Devices with other addresses and server names work the same, but you will need to adapt the dns settings below if you want to use a different naming scheme. I wanted all devices to end with *.pnet.

The referenced snippet proxy-defaults.conf applies decent default settings and saves us a few copy-paster (via this blog post).

# snippets/proxy-defaults.conf
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_redirect off;
proxy_http_version 1.1;

# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

So far so good.

The reverse proxy should be running but we have no real way to check it yet.

DNS via Pi-hole

The next step is to make sure that the reverse proxy can be discovered on the local network. Unfortunately, I did not find a way to do this natively using just the fritzbox router (AVM has removed the option to configre local DNS long ago).

The next best better option is to use a Pi-hole, as it comes with a DNS server — and all my mobile devices that lack native adblockers use it anyway. If you run an OpenWRT router, the procedure should be the similar.

In my case, the Pi-hole is just another docker container, running on the same machine. Note that I am running its web interface on port 8080 to keep 80 free for nginx.

For completeness:

# docker compose

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8080:80/tcp"
    dns:
      - 127.0.0.1
      - 8.8.8.8
    volumes:
      - '~/docker/pihole/etc-pihole:/etc/pihole'
      - '~/docker/pihole/etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped

Now, Pi-hole has an option to add local DNS entries vai the GUI. For Example, we could add portainer.tiny.pnet and map it to 192.168.0.42:9000. But then we have to add each device twice, once in Pi-hole and once in nginx.

To avoid the robotic work, I used a wildcard. While not possible via the GUI (see this reddit), all that is needed are two lines in a config. I created a new file in Pi-hole’s dnsmasq directory:

# /etc/dnsmasq.d/02-pnet.conf

address=/*.pnet/
address=/*.pnet/192.168.0.42

Restart Pi-hole after the config changes, and we are almost done.

What remains to do is to tell your devices to use Pi-hole as their DNS server. It does may or may not to be the primary one, the reverse proxy should work in either case. However, if a second DNS is configured, the ad-blocking won’t work. One could also set the Pi-hole as the primary (or secondary) DNS server in the fritzbox to expose it to all network devices.

To confirm that the DNS part is working, nslookup should return something like this:

> nslookup portainer.tiny.pnet

Server:		100.100.100.100
Address:	100.100.100.100#53

Name:	    portainer.tiny.pnet
Address:    192.168.0.42

Split-DNS in Tailscale

In my case, using tailscale, the steps to get the reverse proxy also working when not at home were fairly simple.

  • Login to the tailscale admin console and add a DNS entry
  • Admin console > DNS > Add Nameserver > Custom
    • Add Pi-hole ip
    • Enable Split DNS
    • Set domain to pnet

Notes

  • To reach my home-assistant container, I had to convince it to play nice with the reverse proxy. See here

Links