Super Pi Part 4: NGINX

Posted on April 11, 2021

My next task was to proxy the Pi-Hole UI with NGINX instead of Lighttpd, following this guide with a few customizations. My main reason for moving to NGINX is that I wanted to host some household HTML documents alongside Pi-Hole and any other services I might add, all under different domains. I have used NGINX before to handle redirecting traffic to HTTPS and proxying it to a non-SSL application with some success. It was a steep learning curve, but I grew pretty fond of NGINX and want to use it again. I haven't gone through it yet, but I found an HTTPS Introduction that should go into more detail on all the steps I'll be going through. For my use case here I'll cover building proxies for Pi-Hole, Node-RED, and a directory of HTML files.

Pi-Hole

The first step is to move Pi-Hole from Lighttpd to NGINX. Luckily, they share most of the same dependencies. This was all I needed:

sudo apt install nginx php7.3-fpm apache2-utils

From there, I disabled Lighttpd and enabled NGINX and php7.3fpm:

sudo systemctl disable lighttpd
sudo systemctl enable php7.3fpm
sudo systemctl start php7.3fpm
sudo systemctl enable nginx
sudo systemctl start nginx

The guide suggests overwriting the default file, but I find this file has some helpful comments for tinkering with later. I recommend making a copy of the default file and calling it pihole:

cd /etc/nginx/sites-available/
sudo cp default pihole

If you're not familiar with NGINX, the documentation advises you create a config file in sites-available and create a symbolic link to it in sites-enabled. When NGINX boots up, it looks in sites-enabled and traces the links back to their respective files in sites-available. This makes it easy to make backups and maybe iterations of configs in sites-available without having to worry about them affecting my proxy. I'll create that symbolic link now:

sudo ln -s /etc/nginx/sites-available/pihole /etc/nginx/sites-enabled/pihole

Since I don't plan on using the default, let's remove it from sites-enabled:

sudo rm /etc/nginx/sites-enabled/default

Now, NGINX operates under the www-data service account, so it's a good idea to make that account own anything NGINX is going to be dealing with. For my use case, this means everything in /var/www/html/:

sudo chown -R www-data:www-data /var/www/html

Pi-Hole itself operates under the pihole service account, not www-data so we might have some issues here. Just add pihole to the www-data group:

sudo usermod -aG pihole www-data

With this setup out of the way, I moved to my pihole config file for NGINX at /etc/nginx/sites-available and replaced its contents with the server block from the guide:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;
    server_name _;
    autoindex off;

    index pihole/index.php index.php index.html index.htm;

    location / {
        expires max;
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        fastcgi_param FQDN true;
        auth_basic "Restricted"; # For Basic Auth
        auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth
    }

    location /*.js {
        index pihole/index.js;
        auth_basic "Restricted"; # For Basic Auth
        auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth
    }

    location /admin {
        root /var/www/html;
        index index.php index.html index.htm;
        auth_basic "Restricted"; # For Basic Auth
        auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth
    }

    location ~ /\.ht {
        deny all;
    }
}

Be sure to test your config syntax once you've modified it:

sudo nginx -t

And restart NGINX for the changes to take effect:

sudo systemctl restart nginx

Note in the config that we're securing the UI with a .htpasswd file, so we need to add a user to it:

sudo htpasswd -c /etc/nginx/.htpasswd admin

This is a separate user from the one that logs into pihole, but I suggest making the passwords the same. I don't see a good reason for making another password to keep track of.

If all you want to do is host Pi-Hole, then that's pretty much it. You should be able to log into Pi-Hole and see it do its thing. Since I want to host other HTML files alongside Pi-Hole, I have a little more to do. Before that, though, I want to register some domain redirects from within Pi-Hole. If you want to do the same, then spin up Pi-Hole's web page and on the left menu, there's a Local DNS heading. Click it and click DNS Records. Let's make an entry for the Pi-Hole UI. It's a little meta, but it works. Under Domain:, put something descriptive, I'll use my.pihole.local. Try to avoid using an existing domain on the internet you would actually want to visit. In the IP Address: field, put the IP of your Pi. Going with the example above, mine would be 192.168.0.23. Save that, and now when you navigate to http://my.pihole.local, you'll be redirected to whatever's hosted on port 80 of your Pi. It should be the Pi-Hole page that asks if you meant to hit the admin page. You can add some other domains to anything else you want to host. I'm hosting Node-RED and a sort of journal, so I'll make two more entries: my.journal.local and my.node-red.local, both pointing to 192.168.0.23.

I want to put the HTML of my journal alongside the Pi-Hole documents in /var/www/html, but I'd prefer if everything related to Pi-Hole were in its own pihole directory. Unfortunately, there's already a pihole directory, so this step is kind of complicated. I went into the existing pihole directory and saw that there were two files: blockingpage.css and index.php. I created another pihole directory and moved these two files into it. Since I had to use sudo to create the directory, it's owned by root. Like everything else in /var/www/html, I want the new pihole directory to be owned by www-data. Let's fix this:

sudo chown www-data:www-data /var/www/html/pihole/pihole

I then moved up a directory to /var/www/html/ and moved the admin folder into the parent pihole folder. Now Pi-Hole's HTML is isolated, but I broke NGINX in the process because it's looking for things in places where they are no longer located. Let's modify /etc/nginx/sites-available/pihole:

server {
    listen 80;
    listen [::]:80;

    root /var/www/html/pihole;
    server_name my.pihole.local;
    autoindex off;

    index pihole/index.php index.php index.html index.htm;

    location / {
        expires max;
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php7.3-fpm.sock;
        fastcgi_param FQDN true;
        auth_basic "Restricted"; # For Basic Auth
        auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth
    }

    location /*.js {
        index pihole/index.js;
        auth_basic "Restricted"; # For Basic Auth
        auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth
    }

    location /admin {
        root /var/www/html/pihole;
        index index.php index.html index.htm;
        auth_basic "Restricted"; # For Basic Auth
        auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth
    }

    location ~ /\.ht {
        deny all;
    }
}

Since I'm going to be hosting more things under different domain names from the same device, I didn't want to specify a default_server, so I removed that from the listen directives. Next, I changed the server_name directive to the domain I specified in Pi-Hole's DNS Records (my.pihole.local). Finally, I changed the root directive to point to /var/www/html/pihole instead of /var/www/html in the server and admin blocks. As always, I tested my syntax and restarted nginx. I navigated to http://my.pihole.local in my browser and everything worked.

Node-RED

Setting up Node-RED was new terrain for me, as I've never dealt much with websockets. Fortunately, NGINX has a fairly straightforward implementation and documentation for proxying them. I made a new config file in /etc/nginx/sites-available/ and called it node-red. This is the config I adapted from the blog post:

map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}

server {
  listen 80;
  listen [::]:80;
  server_name my.node-red.local;

  location / {
    proxy_pass http://websocket;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
  }
}

upstream websocket {
  server localhost:1880;
}

The map directive is used to create new variables that NGINX doesn't know about. Here, I'm creating two variables to use in the location block. As for what's inside the block, I can't provide too much insight, those lines I copied from the document. In the server block, I'm doing the same thing as with Pi-Hole, listening on port 80, but this time I want to handle requests looking for my.node-red.local. I don't have any root or index directives because I'm not pointing to any files. Instead, I want to proxy to a service listening on a port. I accomplished this by setting the proxy_pass directive to http://websocket, which is an upstream I define after the server block. I took the rest of the configuration pretty much straight from the blog post. The Upgrade and Connection headers are required for websocket to be handled properly, but the Host and X-Forwarded-For headers are mostly just metadata I might use later.

The upstream websocket block just needs to know where to connect the socket to; in my case this is port 1880 (the port that Node-RED runs on by default) on localhost.

I created the symbolic link to /etc/nginx/sites-enabled/, saved, and tested using sudo nginx -t. After seeing that everything looked okay, I restarted NGINX as above and navigated to http://my.node-red.local only to find Node-RED's web interface!

Hosting Plain Old HTML Files

The last thing I wanted to proxy was a directory of HTML files. To keep things tidy, I made yet another configuration file at /etc/nginx/sites-available/journal and put this simple server block inside:

server {
    listen 80;
    listen [::]:80;
    root /var/www/html/journal;
    index index.html;
    server_name my.journal.local;

    location / {
        try_files $uri $uri/ =404;
    }
}

All of this should look familiar. The root is pointing to my journal directory and the server_name directive is now the my.journal.local value I set in Pi-Hole's DNS record. Since I'm only exporting HTML, the index directive will only need the one file, index.html, and now NGINX has all the information it needs to make sure requests coming into the Pi go to the right content. I wrapped up by navigating to each page in my browser to make sure everything works and called it a day.