How to setup Pi-hole and the UniFi controller on Raspberry Pi using Docker Compose

And here, like a good DevOp, I begin to completely overengineer the solution...

How to setup Pi-hole and the UniFi controller on Raspberry Pi using Docker Compose
Hey friends! If you already deep into docker and just want an example docker-compose.yml to work from for your project, skip to the end or download it from my Github repo

I've been thinking for a long time that I wanted to setup my own Pi-hole for some ad blocking and maybe some additional sketchy website blocking in my home. Now that the kids are getting older and have access to chrome books and full-on computers that let them climb the heights of internet knowledge, I want to at least provide some protection from the skummy depths and darkness the internet contains. Or at least delay ad companies from creating a total picture of their personality before they can even drive.

And if that was all I wanted to do, this would have been a quick after-work project to get up and running with a few other fidley network bits to adjust for DNS and DHCP. But I also want to update my home wifi and really need an updated UniFi controller that I wanted back behind my firewall. And those should both be able to run on a Raspberry Pi just fine. And here, like a good DevOp, I begin to completely overengineer the solution...

First, I don't want to have to deal with big install processes and application files located all over the place. I definitely don't want to have to remember all the /etc/config files and settings to get started. And I probably need some sort of reverse proxy to make sure the websites work properly. I know! Docker! If I use docker-compose I should be able to get everything I need configured in a single text file, stored in my home directory, and everything should run and reboot automagically.

The great thing is, all of the information to do this exists on-line, unfortunately, not all in the same place. So after hours of searching and typing and tweaking, I wanted to offer a single resource to get this all up and running for the next person.

Pre-Requisits

This guide is making some assumptions about your initial setup and skills:

  • Raspberry Pi 3 or later (arm64)
  • Imaged with a server or Lite OS image (all console no GUI)
  • Terminal text editor of choice installed/configured
  • Pi's networking is all configured in its final state. Static IP, interfaces, DNS fallbacks, the whole bit.
  • docker and docker-compose are installed and the docker service is registered and running
  • Your user (either pi or the custom user of your choice) is added to the docker group so you can run docker without root/sudo

So if you've got all that, you should be ready to start drafting your compose file.

Knee Deep in YAML

I started at the official Docker pi-hole repository which gives some great examples and sample configurations. I'm going to go through each step of configuration for thoroughness, but if you just want a completed docker-compose.yml, skip to the end of this article and note the comments about where to edit with your personal information. So first, we want to just go through getting the pihole service running:

version: "3"

# https://github.com/pi-hole/docker-pi-hole/blob/master/README.md

services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "67:67/udp"
      - "80:80/tcp"
      - "443:443/tcp"
    environment:
      ServerIP: 192.168.###.###
      TZ: 'America/New_York' # Replace with your timezone
      # WEBPASSWORD: 'enter custom password and uncomment, or random'
    # Volumes store your data between container upgrades
    volumes:
      - './pihole/etc-pihole/:/etc/pihole/'
      - './pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/'
      # run `touch ./var-log/pihole.log` first unless you like errors
      - './var-log/pihole.log:/var/log/pihole.log'
    # Recommended but not required (DHCP needs NET_ADMIN)
    #   https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
    cap_add:
      - NET_ADMIN
    restart: unless-stopped
  

docker-compose.yml for pihole only

The above is sufficient to get you going with just pihole. From here a nice docker-compose up -d will launch pihole detached from your console and you could login from your web browser from there. There's nothing too tricky here, but ensure you set your ServerIP, TZ, and WEBPASSWORD environment variables to your liking. But only getting pihole running is not why we're here, so next we want to configure a second container service for the UniFi.

I found the info for this one in a linuxserver.io repository on github. Their example was a v2.x docker compose file, but very little had to be done to update it for version 3:

version: '3'

unifi-controller:
    image: ghcr.io/linuxserver/unifi-controller
    container_name: unifi-controller
    ports:
      - "3478:3478/udp"
      - "10001:10001/udp"
      - "8080:8080"
      - "8443:8443"
      - "1900:1900/udp"
      - "8843:8843"
      - "8880:8880"
      - "6789:6789"
      - "5541:5541"
    volumes:
      - './unifi/config:/config'
    restart: unless-stopped

docker-compose.yml for the UniFi controller only

If you took a look at the linked repository, you'll notice that I excluded a few settings and tweaked it a bit here and there. Largely this was due to compatibility issues that simply don't exist anymore. Nothing here is special, there are no install-specific variables that need to be configured as all of the UniFi config is done through the web interface.

Here's where the tricky bits start. If you've explored the pi-hole/docker-pi-hole git repository you'll find this great example of a configuration complete with reverse proxy for hosting multiple sites. But the existing jwilder/nginx-proxy only has an image on Docker Hub for amd64 architecture. After digging around a bit, it looks like I wasn't the first to run into this, and thankfully Budry took Jwilder's existing work and rebuilt the images on multiple architectures, so now we can start to tie this all together.

First, I want to get the proxy container info entered:

version: "3"

services:
  jwilder-proxy:
    image: budry/jwilder-nginx-proxy-arm
    ports:
      - '80:80'
      - '443:443'
    environment:
      DEFAULT_HOST: pihole.domain.tld
    volumes:
      - '/var/run/docker.sock:/tmp/docker.sock'
    restart: always

...

Starting docker-config.yml with the base proxy settings

You'll notice Nginx needs to bind to ports 80 and 443 which is going to conflict with the pihole config earlier, we'll address that in a second. Most importantly, though, you need to make sure your DEFAULT_HOST is set to your Pi-hole. This is important for some Pi-Hole functionality.

Next, let's adjust the Pi-Hole config accordingly, and add some environment variables to help direct the reverse proxy:

...

  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "67:67/udp"
      - "8053:80/tcp"
      - "9443:443/tcp"
    environment:
      ServerIP: 192.168.###.###
      TZ: 'America/New_York'
      PROXY_LOCATION: pihole
      VIRTUAL_HOST: pihole.domain.tld
      VIRTUAL_PORT: 80
      WEBPASSWORD: 'SomePassword'
    volumes:
      - './pihole/etc-pihole/:/etc/pihole/'
      - './pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/'
      - './var-log/pihole.log:/var/log/pihole.log'
    cap_add:
      - NET_ADMIN
    extra_hosts:
      - 'domain.tld:192.168.###.###'
      - 'pihole pihole.domain.tld:192.168.###.###'
      - 'unifi unifi.domain.tld:192.168.###.###'
    restart: unless-stopped
...

pihole service adjusted for nginx

Not a lot has changed, but what has is important. First note the ports adjusted. We're now mapping 8053:80 to free up host port 80, and 9443:443 to free up 443. I used the 9000 range instead of the standard 8000 since 8443 is needed for UniFi.

Next, you'll see the aditional environment variables VIRTUAL_HOST and VIRTUAL_PORT these let docker-gen know how to configure the proxy for nginx. The host could be named as you want (if for some reason you don't like pihole) but port 80 is important.

Finally, a whole section has been added under extra_hosts: this is where you're going to list hostname FQDN:IP.Address for each host (beyond nginx) you want to spin up in the docker environment. It's listed here as this is our Default Host. For our purposes, we need the primary domain and the pihole and unifi subdomains.

And with that, we can wrap it up with the changes needed for the unifi-controller bits:

...
  unifi-controller:
    image: ghcr.io/linuxserver/unifi-controller
    container_name: unifi-controller
    ports:
      - "3478:3478/udp"
      - "10001:10001/udp"
      - "8080:8080"
      - "8443:8443"
      - "1900:1900/udp"
      - "8843:8843"
      - "8880:8880"
      - "6789:6789"
      - "5541:5541"
    environment:
      PROXY_LOCATION: unifi
      VIRTUAL_HOST: unifi.domain.tld
      VIRTUAL_PORT: 8443
      VIRTUAL_PROTO: https
    volumes:
      - './unifi/config:/config'
    restart: unless-stopped

unifi-controller configuration adjustd for nginx

This one is even easier with the changes you might have predicted at this point, PROXY_LOCATION, VIRTUAL_HOST, VIRTUAL_PORT are all familiar from the above and we make the addition of VIRTUAL_PROTO since unifi wants encrypted traffic and otherwise will try to connect via http which will make unifi sad.

The Completed YAML File

Now for what you might be waiting for, the completed docker-compose.yml (This one is sanitized and full with commentary for those skipping right to this point):

version: "3"

services:
  jwilder-proxy:
    image: budry/jwilder-nginx-proxy-arm
    ports:
      - '80:80'
      - '443:443'
    environment:
      DEFAULT_HOST: pihole.domain.tld
    volumes:
      - '/var/run/docker.sock:/tmp/docker.sock'
    restart: always

  pihole:
  # https://github.com/pi-hole/docker-pi-hole/blob/master/README.md
    image: pihole/pihole:latest
    container_name: pihole
    # For DHCP there is some advanced router configuration. Do not use host mode networking or it will conflict with nginx and the unifi-controller
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "67:67/udp"
      - "8053:80/tcp"
      - "9443:443/tcp"
    environment:
      ServerIP: ###.###.###.###
      TZ: 'America/New_York' #Adjust for your timezone
      PROXY_LOCATION: pihole
      VIRTUAL_HOST: pihole.domain.tld
      VIRTUAL_PORT: 80
      # WEBPASSWORD: 'enter a password and uncomment, otherwise random'
    # Volumes store your data between container upgrades
    volumes:
      - './pihole/etc-pihole/:/etc/pihole/'
      - './pihole/etc-dnsmasq.d/:/etc/dnsmasq.d/'
      # run `touch ./var-log/pihole.log` first unless you like errors
      - './var-log/pihole.log:/var/log/pihole.log'
    # Recommended but not required (DHCP needs NET_ADMIN)
    # https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
    cap_add:
      - NET_ADMIN
    extra_hosts:
      - 'domain.tld:###.###.###.###'
      - 'pihole pihole.domain.tld:###.###.###.###'
      - 'unifi unifi.domain.tld:###.###.###.###'
    restart: unless-stopped

  unifi-controller:
    image: ghcr.io/linuxserver/unifi-controller
    container_name: unifi-controller
    ports:
      - "3478:3478/udp"
      - "10001:10001/udp"
      - "8080:8080"
      - "8443:8443"
      - "1900:1900/udp"
      - "8843:8843"
      - "8880:8880"
      - "6789:6789"
      - "5541:5541"
    environment:
      PROXY_LOCATION: unifi
      VIRTUAL_HOST: unifi.domain.tld
      VIRTUAL_PORT: 8443
      VIRTUAL_PROTO: https
    volumes:
      - './unifi/config:/config'
    restart: unless-stopped

The complete docker-compose.yml for UniFi Controller, Pi-Hole and the Nginx reverse proxy

At this point, you can run docker-compose up -d and with a little patience you'll have all your services up and running on your Pi. It freaked me out a couple times where I thought the UniFi controller was broken and not working, but it looks like it just takes a little longer to get started than Pi-Hole. So give it a few minutes before you try to browse the web portals, but once all the services are up you should be able to navigate in your web browser using the FQDNs.

Because your home domain is probably an invalid TLD, you'll likely have to type a full URL like http://pihole.domain.tld or https://unifi.domain.tld to avoid your browser's search functionality. If they don't resolve, it's probably because you're not using your pihole as your DNS server yet. Note: the UniFi controller might be a little wonky without directing it to 8443 since that's the expected port to communicate on. I've been trying to get it to play a little nicer, but haven't gotten there at time of writing. I'll update if I do get it all working 100%.

With this, you're able to use all your services! If you have any issues with this or find any way to improve on it I'd love to find out; leave a note on my github repo!

P.S. Advanced Networking

This is where that note about host mode networking in the pihole container config becomes relevant. For the Pi-hole server to recieve DHCP requests, it generally needs to be wide open to requests and broadcasts, but since it's packed into docker's virtual network, this can creates challenges and since we're using the reverse proxy and hosting multiple services on docker, we can't just attach the entire network interface from the host Pi without causing conflicts. It's possible this will all work without changes, but if not, we need to tweak the network to ensure DHCP requests are making their way to your pihole service.

The answer here is that you'll need to configure a DHCP relay on your network. This is a service that will relay the DHCP request to the specific IP that should be answering. This is done to allow a DHCP request to be answered by a server outside of the requestor's broadcast domain. How you do this depends on the equipment you have available. If your router is sufficiently advanced it may have this as a configurable setting otherwise, it is possible to configure another small docker service to act as a relay. Since I have a Ubiquiti Edge Router X, all I had to do was login and make a two-line config change and save it:

> set service dhcp-relay interface switch0 
> set service dhcp-relay server ###.###.###.###
> commit ; save

ubiquiti router configuration

Here, the IP address used is the same as the static IP address of your Pi being used in all the configurations previously.

So if you were having any DHCP issues, those should all be solved with the relay.