Skip to content

Instantly share code, notes, and snippets.

@maxkagamine
Last active June 30, 2024 19:00
Show Gist options
  • Save maxkagamine/5b6c34db6045d6413db3b333d6d2bae2 to your computer and use it in GitHub Desktop.
Save maxkagamine/5b6c34db6045d6413db3b333d6d2bae2 to your computer and use it in GitHub Desktop.
Guide to running an nginx reverse proxy on Unraid with a Let's Encrypt wildcard cert, using the official nginx and certbot Docker images.

Nginx & certbot on Unraid

Here's a guide to running an nginx reverse proxy on Unraid with a Let's Encrypt wildcard cert (which can cover the Unraid web gui too), using the official nginx and certbot Docker images.

Other options:

  • caddy — popular nginx alternative with built-in automatic Let's Encrypt
  • pomerium — all-in-one reverse proxy, SSL, and OAuth-based login (compare to Caddy Security)
  • nginx-certbot — parses your nginx configs and manages certbot for you (see the original version of this gist for a script to copy your cert to Unraid)
  • swag — used to be called letsencrypt; has other things in it like PHP & fail2ban

Tip: The user script below for updating the Unraid cert can be used by itself if you choose any of these instead (just remove the docker restart nginx).

1. Prerequisite: a domain

This assumes you have a) a domain pointed at your home network, and b) a DNS server supported by certbot. If not, buy a domain, set up Cloudflare as the nameserver, and use ddns-updater to keep it pointing at your IP. The rest of the guide will assume you're using Cloudflare and that your domain is example.com (change those references as you see them).

For subdomains, create a CNAME pointing * to the domain (or maybe don't, see "A note about Windows" below...). The DNS tab in Cloudflare should look like this:

Type Name Content Proxy status TTL
A example.com your ip DNS only 10 min
CNAME * example.com DNS only 1 day

To use the Let's Encrypt cert for the Unraid web gui as well, your router should be configured to use your domain such that <hostname>.example.com resolves to <hostname>'s IP. (In UniFi, this is under Networks → LAN → Advanced.) You'll also need to go into Unraid's "Management Access" settings and set "Use SSL/TLS" to Yes and "Local TLD" to your domain (e.g. example.com, without the Unraid hostname).

1b. A note about Windows...

Windows likes to append your domain to DNS lookups. If the external DNS has a wildcard CNAME, sometimes foo.example.com, which should refer to the local hostname foo if example.com is your network domain, instead gets looked up as foo.example.com.example.com, which resolves via that CNAME to your public IP, which gets port-forwarded via your router to the wrong place, and you get a connection refused. Running ipconfig /flushdns seems to fix this temporarily (it's possible after rebooting, my router received the DNS request for the Unraid server before its hostname got added to its DNS tables and thus forwarded the lookup to the external DNS, and that response got cached).

What you can do instead is a) add your Unraid's subdomain to the external DNS, pointing it at its internal IP so that it resolves correctly even if it gets looked up wrong; b) avoid the wildcard and explicitly add CNAMEs for each subdomain you're going to reverse proxy; or c) run your own local DNS server in a docker container.

Having lost many hours to this, I suggest (b).

2. The nginx container

Docker → Add container, toggle "advanced view" in the top-right, and set as follows:

Set your router to forward ports 80 and 443 to 8080 and 8443 on the server, respectively (Unraid will need a fixed IP).

If your router for some reason can't map to a different port, or you want nginx to have a separate IP on your network, use the "Custom: br0" network (not to be confused with Docker's default bridge, this is a macvlan using a Linux bridge network that essentially acts like the host and container are plugged into a physical network switch: docker network ls).

You should be able to hit up your domain now and see the default nginx page.

3. Certbot user script

Save the Certbot script below to /boot/config/plugins/user.scripts/scripts/certbot/script. Be sure to find & replace all instances of "example.com" and put your email in. Set it to run daily.

For Cloudflare, you'll need to create /mnt/user/appdata/letsencrypt/cloudflare.ini; for other dns providers, change those options and the image name. In any case, see the docs here.

IMPORTANT! The wildcard must come first so that it becomes the cert's CN (common name). This tells Unraid that it should use <hostname>.example.com; otherwise when the system reboots, it tries to redirect to example.com, thinking that's its url, which ends up resolving to the nginx server, which isn't running, and so you can't access the web ui anymore! If this happens to you as it did to me, rm /boot/config/ssl/certs/* && reboot. Temporarily change --keep-until-expiring to --force-renewal to fix the cert after swapping the domains.

4. Nginx config

Drop the ssl.conf file below in /mnt/user/appdata/nginx (again, replacing "example.com"). Any reverse proxy configs you add now only need to listen to 443 and will get the wildcard cert & redirect from http automatically.

(dhparams.pem can be created using openssl dhparam -out /mnt/user/appdata/letsencrypt/dhparam.pem 4096, or you can remove that line.)

5.

Rejoice

#!/bin/bash
#name=Certbot
#description=Renews the example.com SSL cert.
#arrayStarted=true
#backgroundOnly=true
set -eo pipefail
LE_DIR=/mnt/user/appdata/letsencrypt
CERT_DIR=$LE_DIR/live/example.com
UNRAID_BUNDLE=/boot/config/ssl/certs/certificate_bundle.pem
run() {
# Store the old cert to check if changed
[[ -f $CERT_DIR/cert.pem ]] && old_cert=$(< $CERT_DIR/cert.pem)
# Run certbot
docker run --rm -v $LE_DIR:/etc/letsencrypt certbot/dns-cloudflare certonly \
--non-interactive \
--keep-until-expiring \
--agree-tos \
--key-type ecdsa \
--email 'YOUR EMAIL HERE' \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d '*.example.com' -d 'example.com'
# Check if cert was renewed
if [[ $(< $CERT_DIR/cert.pem) != "$old_cert" ]]; then
# Update web gui cert
echo 'Updating web gui cert'
cat \
$CERT_DIR/cert.pem \
$CERT_DIR/fullchain.pem \
$CERT_DIR/privkey.pem > "$UNRAID_BUNDLE"
/etc/rc.d/rc.nginx restart
# Restart nginx docker container
echo 'Restarting nginx docker container'
docker restart nginx
fi
}
# Capture log for error report while still writing to stdout
# https://stackoverflow.com/a/16292136
{ log=$(run 2>&1 | tee /dev/fd/5); } 5>&1 || err=$?
if (( err > 0 )); then
notify \
-s "$(sed -n 's/^#name=//p' "${BASH_SOURCE[0]}") script failed" \
-d "Exit code $err (logs below)" \
-i 'alert' \
-m "$log"
fi
# Wildcard cert
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
ssl_dhparam /etc/letsencrypt/dhparam.pem;
# Redirect everything to https
server {
listen 80 default_server;
return 301 https://$http_host$request_uri;
}
# Reject unconfigured domains
server {
listen 443 ssl default_server reuseport;
ssl_reject_handshake on;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment