Post-Mortem: Replacing the default reverse proxy on Synology NAS

The full process of replacing the default reverse proxy on a Synology NAS; the issues I encountered and how I resolved them.

Post-Mortem: Replacing the default reverse proxy on Synology NAS
Photo by Bruno Wolff / Unsplash

Over the weekend, I set out to replace the default reverse proxy on my Synology NAS. Don't get me wrong, the reverse proxy works great for basic needs, but my needs are just slightly above basic, so the default one just didn't cut it.

For context, my Synology NAS is the brain of the house. I have 20+ services running off of it - e.g. home automation, media acquisition (if you catch my drift) and media streaming, Dropbox replacement, coding server (Jupyter / VSCode), DNS Server, recipe board, ad blocking, etc...

I want to be able to access all these services without having to remember the IP address & port that they're running from.

This is a write up of what I tried to do, issues that I ran into, and how I resolved them.

There's a TL;DR at the bottom.


Objectives:

  1. Spin up a reverse proxy to forward queries to the relevant services within my home network (e.g. http://jellyfin.lan will take me to my Jellyfin service at 192.168.0.100:1234)
  2. The reverse proxy used will be NGINX Proxy Manager
  3. A separate IP address is needed for this as port 80 on the Synology host is already used by the default reverse proxy.

That's it, let's keep it nice and simple. (spoiler alert: that was not my experience going into this, but I hope this write-up can save you from a few gray hairs)

Vision:

I'm a visual learner, so it was important for me to have a visual diagram of how I want things to work, and re-adjust as I go along until I get the full picture.

The flow should be:

  1. As the end user, I can type in http://serviceName.lan/ into my browser
  2. My DNS server (Pi Hole) will pick up that request, and point the request to the IP address in the DNS record - that would be the reverse proxy's IP
  3. My reverse proxy will then resolve the domain serviceName.lan and show me the service hosted on the relevant IP and port, e.g. 192.168.0.100:9000

Setting up Nginx Proxy Manager in Docker

I used Docker and Docker-Compose to set up Nginx Proxy Manager; a docker-compose.yml file can be found below (a simplified version of my final stack).

  1. Create a folder to store the reverse proxy files: /volume1/docker/npm (or wherever you normally store your docker data)
  2. Create a file called docker-compose.yml within this new folder (see below)
  3. Create 2 sub-folders called data and letsencrypt
  4. Run command docker-compose up -d

Docker-Compose.yml:

version: '2' # version 2 is necessary. v3 does not support ipam

services:
 proxy:
   image: 'jc21/nginx-proxy-manager:latest'
   container_name: proxy
   restart: unless-stopped
   networks:
     macvlan_network:
       ipv4_address: 192.168.0.201
   ports:
      # Public HTTP Port:
     - '80:80'
      # Public HTTPS Port:
     - '443:443'
      # Admin Web Port:
     - '81:81'
   environment:
     - DB_SQLITE_FILE: "/data/database.sqlite"
   volumes:
     - ./data:/data
     - ./letsencrypt:/etc/letsencrypt

networks:
 macvlan_network:
   driver: macvlan
   driver_opts:
     parent: ovs_eth0
   ipam:
     config:
       - subnet: 192.168.0.0/24
         gateway: 192.168.0.1
         ip_range: 192.168.0.200/30
docker-compose.yml for reverse proxy and macvlan

Docker-compose.yml explained:

  • A container called proxy is to be created, using the image jc21/nginx-proxy-manager with the tag latest
  • SQLite database is used
  • I am creating a new Docker macvlan network called macvlan_network
  • The new network is linked to ovs_eth0 (the ethernet switch on my NAS, you can check yours by typing ifconfig in terminal)
  • 192.168.0.201 is the IP I am assigning to the reverse proxy container
  • 192.168.0.200/30 is the IP range for this Docker macvlan

Issue: multiple Docker MacVLANs

If you don't have a pre-existing Docker MacVLAN, then this is a non-issue, and the above steps should work for you.

I ran into this error whilst spinning up the Docker container:

Failed to allocate gateway (https://192.168.0.1): Address already in use

Turns out, I can't have more than one Docker MacVLAN for each gateway / network switch! I already had one for PiHole so it's not letting me create another one for the reverse proxy.

Solution: Use the existing MacVLAN that I created for PiHole

I added the reverse proxy to my PiHole docker-compose stack and just assigned it with an IP address. So now both my PiHole and NGINX Proxy Manager are in the same stack and using the same MacVLAN, just with different IP addresses.

Putting Pihole and Nginx Proxy manager in the same docker stack and same macvlan network.

You can, of course, do this in different ways. e.g. rather than setting up network AND the services within the same docker-compose file, you could try setting up a standalone Docker macvlan, then in your docker-compose files just reference this network and assign an IP.

FYI: This is my final docker-compose file:

version: '2' # version 2 is necessary. v3 does not support ipam

# Reference: https://www.reddit.com/r/synology/comments/gmfta3/pihole_5_on_synology_using_docker_simplified_with/

services:
 pihole:
   container_name: pihole
   image: pihole/pihole:latest
   networks:
     macvlan_network:
       ipv4_address: 192.168.0.200
   #cap_add:
   #  - NET_ADMIN # add if using DHCP in pihole
   volumes:
     - "./pihole/:/etc/pihole/"
     - "./dnsmasq.d/:/etc/dnsmasq.d/"
   dns:
     - 127.0.0.1
     - 8.8.8.8
   ports:
     - 443/tcp
     - 53/tcp
     - 53/udp
     - 67/udp
     - 80/tcp
   environment:
     TZ: Europe/London
     ServerIP: 192.168.0.200
     WEBPASSWORD: !WebPass1234! #change this!
   restart: unless-stopped
   
 npm:
   image: 'jc21/nginx-proxy-manager:latest'
   container_name: proxy
   restart: unless-stopped
   networks:
     macvlan_network:
       ipv4_address: 192.168.0.201
   ports:
      # Public HTTP Port:
     - '80:80'
      # Public HTTPS Port:
     - '443:443'
      # Admin Web Port:
     - '81:81'
   environment:
     DB_SQLITE_FILE: "/data/database.sqlite"
     DISABLE_IPV6: 'true'
   volumes:
     - ./npm/data:/data
     - ./npm/letsencrypt:/etc/letsencrypt

networks:
 macvlan_network:
   driver: macvlan
   driver_opts:
     parent: ovs_eth0
   ipam:
     config:
       - subnet: 192.168.0.0/24
         gateway: 192.168.0.1
         ip_range: 192.168.0.200/30

Configuring the local DNS server

I then setup multiple DNS entries on my Pi Hole:

  • A records: npm.lan --> 192.168.0.201 (IP of my Nginx Proxy Manager container)
Example DNS entry: A record
  • CNAME records: ¬†add the addresses for your services as cname records for npm.lan, so that all of them will be pointing to the same reverse proxy. And if the reverse proxy IP needs to be updated for whatever reason, I just need to update the A record for npm.lan.
Example DNS entries: CNAME records

Configuring Nginx Proxy Manager

Now that the Nginx Proxy Manager container is up and the DNS server has been instructed to direct requests to the reverse proxy, let's login and set up the reverse proxy itself.

  1. Visit 192.168.0.201:81
  2. Login as [email protected] with password changeme. You'll be asked to update this on first login.
  3. Setup the proxy hosts by pointing the source URL to the relevant destination*, e.g. home.lan points to 192.168.0.6:5000

Issue: containers in MacVLAN cannot communicate directly with the host

In my setup, both the reverse proxy service and the other services were all on the same Synology NAS. Unfortunately (or fortunately? depends on what you want to achieve I guess), by default, MacVLAN cannot communicate with the host as they are isolated networks.

So that means the current reverse proxy routes don't work.

Nginx Proxy Manager in a MacVLAN cannot speak to the containers on the host

Solution: Creating a MacVLAN bridge on the host

Create a new virtual network bridge (a MacVLAN) on the host itself (i.e. the NAS), and then open a route to the reverse proxy container.

  • Create a macvlan called macvlan0

sudo ip link add macvlan0 link ovs_eth0 type macvlan mode bridge

  • Assign the IP 192.168.0.210 tomacvlan0

sudo ip addr add 192.168.0.210/32 dev macvlan0

  • Make the macvlan active

sudo ip link set macvlan0 up

  • Open up a route to the reverse proxy container

sudo ip route add 192.168.0.201 dev macvlan0

Configuring Nginx Proxy Manager, again

OK, let's try that again.

I updated the reverse proxy entries by pointing them to the macvlan0's IP instead of the host IP.

Reverse Proxy pointing to macvlan0 IP rather than the host IP

Aaaaaand it works!  Reverse proxy successfully replaced.

Jellyfin service on my NAS can be accessed by going to jellyfin.lan from any device (when I'm on the network)

MacVLAN - surviving restarts

Now that I have proven the approach works, I need to make sure the MacVLAN that I created on the NAS is able to survive restarts. So I created a little script with the commands above and made sure it ran when the NAS starts up (using Task Scheduler).

macvlan.sh:

  • Create a file called macvlan0.sh
sleep 60
ip link add macvlan0 link ovs_eth0 type macvlan mode bridge
ip addr add 192.168.0.210/32 dev macvlan0
ip link set macvlan0 up
ip route add 192.168.0.201 dev macvlan0
  • Make it executable

chmod +x macvlan0.sh

Task Scheduler:

  • Open up Task Schedule on the NAS, and then click on Create --> Triggered Task --> User-defined script
  • Enter a name for the task, and make sure the user is set to root.

TL;DR

  • I wanted to replace the Synology reverse proxy with Nginx Proxy Manager (NPM)
  • A new IP was generated for NPM by creating a macvlan on Docker (as port 80 was already taken up by the existing reverse proxy)
  • Found that containers in a Docker MacVLAN cannot communicate with the containers on the host, so reverse proxy didn't work
  • Fixed it by creating a MacVLAN on the host itself, and then opening up a route to the reverse proxy
  • It worked.