You will learn about setting up an NGINX reverse proxy, adding VM disk space, and managing NodeJS apps with pm2
A tutorial series on building a complete home lab system from the ground up that is beginner-friendly, versatile, and maintainable.
Preface
In this segment you will learn about setting up an NGINX reverse proxy, adding VM disk space, and managing NodeJS apps with pm2. I am going to set up an instance of the ‘4t’ app I put together in React, which is a 20, 20, 20 timer for eye health that I use all the time, but you are free to set up any back-end host you wish.
Set up a Web Endpoint
Create a new VM
The first thing we need to do is set up a web app on a new VM to proxy traffic to, so let’s clone our template again.

Use a linked clone and choose a name for this VM.

Go ahead and start the new VM up.

You’ll want to run through the boilerplate steps from part 3 to set a unique hostname and give the new VM some entropy.
Install Dependencies
Let’s get our software suite installed (NodeJS, PM2), run the following commands in a console session on the new VM.
PM2 is a process manager for NodeJS applications, it will handle starting them up at boot time, and restart any processes that crash.
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt dist-upgrade -y
sudo apt install -y nodejs git
npm install -g pm2
Now we will run into a problem, we are almost out of disk space on this VM!

Add Disk Space
You could just as well resize the existing hard disk in most cases, but I want to demonstrate the power and flexibility of LVM here, so I will add a new disk and combine the two.
Head to Datacenter > (Name of your host) > (Name of your VM) > Hardware > Add > Hard Disk.


Back in the VM Console, run the command lsblk
to list the block devices, and you will see the new disk sdb
available.

The following commands will provision the new disk as an LVM physical volume (PV), extend the existing volume group (VG) to span both the new and old disk, and then expand the logical volume (LV) the OS is installed upon to its maximum available size.
First we need to know the existing VG and LV names, we can discover these with the commands sudo vgs
, and sudo lvs
, respectively. In my case the VG is name ubuntu-vg
, and the LV is name ubuntu-lv
.
sudo pvcreate /dev/sdb
sudo vgextend ubuntu-vg /dev/sdb
sudo lvextend ubuntu-vg/ubuntu-lv -l+100%FREE
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
reboot
Since the OS is running on the LV that we extended, it will have to reboot to reflect the changes. I usually just add a new disk for application data if needed and keep it separate from the OS disk to avoid this, but now you’ve seen a taste of LVM’s feature set.
After the reboot, we can see that the block device mounted as /
, which is our LV, has grown to 11GiB in size.

Provision Web App
Now let’s grab the source code for my 4t app and get it configured.
git clone https://github.com/dlford/4t.git
cd 4t
npm install
We’ll need to change the homepage
value in the package.json
file to http://127.0.0.1
because it’s set by default to be deployed on GitHub pages, and that configuration won’t work in our case.
nano package.json

Now that we fixed that, we can build the app.
npm run build
We’ll write a quick script to start the app running with the serve
package from NPM.
touch startup.sh
chmod +x startup.sh
nano startup.sh
npx serve --single --no-clipboard build
Now let’s start that script with PM2.
pm2 start startup.sh --name 4t-app --watch
PM2 needs to be configured to start at boot time, run the command pm2 startup
and PM2 will spit out a command for you to run to get this done, so go ahead and re-type that into the console as shown.
Lastly, save the running configuration for PM2 with the command pm2 save
.
DHCP Reservation
Follow the DHCP Reservation steps from part 3.
Don’t forget to reboot the new VM after setting up DHCP reservation so it will acquire the new IP address.

Configure Reverse Proxy
From a console session on your NGINX VM, let’s create a configuration file for the new reverse proxy.
sudo nano /etc/nginx/sites-available/4t-app.conf
4t-app.conf
server {
listen 80;
server_name 4t.burns.lab;
location / {
proxy_read_timeout 36000s;
proxy_http_version 1.1;
proxy_buffering off;
client_max_body_size 0;
proxy_redirect off;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Powered-By;
proxy_pass_header Authorization;
proxy_pass http://172.16.44.102:5000;
}
}
Note that not all of the options are required for all circumstances, for this particular use case we could’ve omitted most of them, and there are many others that I haven’t listed here as well. I keep all of these options in all of my configurations just so I don’t have to look them up when I need them, I just copy an existing configuration and edit it for the new back-end server. Here’s what they do:
server
: Designate that this block of configuration is for a server, has nothing to do with proxyinglisten 80
: Listen on port 80 for HTTP trafficserver_name 4t.burns.lab
: This configuration applies only to requests for the hostname4t.burns.lab
, make sure to change this to your own hostlocation /
: A location block applies to a specific URL, it is recursive, meaning/page1
would still match the location block for/
, unless there is alocation /page1
also specified in the file.proxy_read_timeout 36000s
: how long to wait before giving up on connecting to the back-end serverproxy_http_version 1.1
: Use HTTP protocol version 1.1, this is needed when the NGINX proxy serves HTTP version 2 but the back-end server only supports version 1.1proxy_buffering off
: Don’t wait to receive the full reponse from back-end, send the response as a real-time streamclient_max_body_size 0
: Set the maximum size of the body of a request, 0 is disabled to allow any sizeproxy_redirect off
: This is off by default, but can be used for URL re-writingproxy_set_header Connection ""
: Overwrite theConnection
header of a request to an empty value, this prevents the request from closing a connection manually which will break keepalive on the back-end host if it is used.proxy_set_header Host $host
: Use the host header from the request when contacting the back-end server, required if the back-end host also serves different content depending on the hostname in the requestproxy_set_header X-Real-IP $remote_addr
: The back-end host will log the request as coming from the reverse proxies IP address, theX-Real-IP
header is used to determine the actual source of the requestproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
: Same as above but a different header nameproxy_set_header X-Forwarded-Host $host
: Same as above, yet another header nameproxy_set_header X-Forwarded-Proto $sheme
: Tell the back-end host if the original request came in over HTTP or HTTPSproxy_hide_header X-Powered-By
: Hide the service name and version information for the back-end serverproxy_pass_header Authorization
: Pass any HTTP Basic Authorization data to the back-end server, which is a dirt simple method of password protecting a web site, it’s still used sometimes but better methods exist today.proxy_pass http://172.16.44.102:5000
: This should be the internal IP address and port for the back-end host that will receive proxied requests
Now we need to symlink the configuration file into the sites-enabled
directory where NGINX looks for active configuration files.
sudo ln -s /etc/nginx/sites-available/4t-app.conf /etc/nginx/sites-enabled/
You should test the configuration with the command sudo nginx -t
, if it comes back successful go ahead and restart NGINX with the command sudo service nginx restart
.
See it in Action
This is where we really start to see the drawback to running an internal private network on Proxmox over a custom home network solution (we will address this in a future segment). If you visit http://(IP address of your pfSense VM)
, you’ll get the default site running on the NGINX server and not the reverse proxied site, because we need the host header to match the configuration file for the proxied site (4t.burns.lab
in my example above).
You’ll need to utilize the hosts
file on your workstation to test this out, the hosts
file is essentially a DNS override, you put in an IP address and hostname, and any communication to that hostname will go to the IP address in the hosts
file rather than the IP address a DNS lookup would return.
The syntax is simple, just the IP address of your pfSense VM, one or more spaces, and the hostname you configured in the NGINX configuration file. In my example this is 10.128.0.27 4t.burns.lab
. You’ll want to add that line to the bottom of the hosts
file on your workstation, which you’ll need to edit as an administrator.
- Windows:
C:\Windows\System32\drivers\etc\hosts
- Linux/Mac:
/etc/hosts
After that change, drop http://(hostname)
into your browsers URL bar (e.g. http://4t.burns.lab
), and you should see the 4t app running behind your NGINX reverse proxy, that’s running behind your pfSense VM, that’s running on your Proxmox host!
