This blog post is about making a "Test" NodeJS Webserver available externally on the internet using Unraid, NGINX, Cors, LetsEncrypt and DuckDNS. Yep that sure is a mouthful and believe me it took me a lot of problem solving, querying, trial and error, tears and hard work to get a working solution.
The main goal of this project is making data from your web server available externally on the world wide web (external to your 'local' network).
Now, the phrase "Test" NodeJS Webserver may leave you intrigued or disappointed or both but to clarify ideally the NodeJS Webserver would be in it's own Docker container and running on Unraid. Unfortunately for you that is for another day (and tutorial) as ideally this elementary NodeJS Webserver will be expanded upon to become a Rest API webserver down the road. But small steps as they say.
Please be aware that this example uses a specific set of software and most importantly NGINX to request the SSL certificate and run a reverse proxy. If you don't use NGINX then just modify your set up to use whatever alternative reverse proxy software you are using, eg, Caddy.
Pre-requisites
For this example I am running:
- A computer running Windows 11.
- Visual Studio Code (installed on Windows PC).
- NGINX Proxy Manager (v1.27.1.2) running on Unraid.
- DuckDNS.
- A registered DuckDNS sub-domain.
- Unraid 6.2.10
- Router
This truly is quite a huge project with multiple moving parts even for the simplest of web servers. It's got everything from port forwarding on your router, reverse proxy via NGINX Proxy Manager, Cross-Origin Resource Sharing (Cors), dynamic DNS (via DuckDNS), SSL encryption (using LetsEncrypt and NGINX Proxy Manager), Unraid, NodeJS, HTML, javascript and lots and lots of testing.
High level process
This is quite the gargantuan project and is best broken down into its smaller components. At the very end you will see how it all comes together. However, the main steps can be roughly defined as:
- Creation of the web server (using NodeJS)
- DDNS - Catering for Non-static IP address
- SSL LetsEncrypt Certification
- Reverse proxy (using NGINX Proxy Manager)
- Cross origin resource sharing (Cors) (using NGINX Proxy Manager)
- Webpage for testing
Conceptually this tutorial looks like this:
Looking at the above diagram we will be:
- Creating the NodeJS web server (the yellow circle) on a local development PC
- Requesting data from the web server from a webpage at the domain www.50plusjourney.com
- Using Nginx for reverse proxy, applying cors and requesting a SSL certificate
- Using DuckDNS to cater for a non-static home IP address
Development Computer Running Windows
A critical component of this tutorial and in fact any tutorial involving NGINX Proxy Manager and making data available outside your local network and out into the world wide web are IP addresses and ports. Yes, everything can get quite messy when you are problem solving as any networking person will know.
Development vs Production: Anyway, why this is important as in this particular project the web server is only a "development" server and not rolled out into "Production" into it's own Docker Container on Unraid. The key here is that the "development" computer is effectively running on a separate network to NGINX Proxy Manager that is running on Unraid.
IP Address of development computer: This means that one of the working parts and essential components of this project is knowing the IP Address of the development computer. Hopefully you have a "static" IP set up otherwise you will have to continually keep track of the IP address when you do your testing further down the track.
As the Development computer is running Windows you can find the IP address of the computer in the Windows Terminal by typing:
Your IP Address: The key here is to look for the IPv4 address as your development computer is known on your network either by an ethernet adaptor or as per this example a wireless adaptor. Whatever the case, note down the IP address as this will be used later. As stated earlier, hopefully your network uses static IP addresses so you won't have to keep running Ipconfig to geet your IP Address.
Node.js Web Server
In the following I will create the most simple Node.js web server ever (or close to it). Stay tuned for a future tutorial when this expands to a Node.js Rest API webserver.
Pre-requisite: "Visual Studio Code" must be installed on the development computer as the Node.js webserver will be coded in Visual Studio Code.
Ports and IP Addresses: Yeah, don't want it to be a beatdown but this is critical data required for creating a successful project.
In this tutorial the webserver will be running on port: 8100.
IP Address: As outlined earlier the Node.js web server will be running on the development computer rather than it's own Docker Container. As such, you have to know the IP address of the development computer. In this tutorial, the IP Address of the development computer is: 192.168.1.111.
NodeJS Web Server: Step by Step
All the following will be performed in Visual Studio Code. The high level process is listed below:
- Create a new folder for your NodeJS web server project.
- Initialize a Node.js project. In the Terminal run code: npm init -y
- Install Middleware dependencies. In the Terminal run code: npm install express
- In the project folder create a new file called 'server.js'
- In the file 'server.js' code the simple NodeJS web server.
- Run the NodeJS web server from the Terminal with the code: node server.js
- Test the NodeJS server is running.
- Requesting an SSL Certificate from LetsEncrypt
- Reverse proxy
- Application of Cors
- a DuckDNS account
- a DuckDNS token (for your account)
- a subdomain (for your DuckDNS account)
- LetsEncrypt is FREE!
- We are already using NGINX for our reverse proxy
- NGINX handles auto renewing of certificates for us
- NGINX makes the process easy
- We will be using NGINX for applying Cors
- Open NGINX.
- Click on SSL Certificates then Add SSL Certificate then Let's Encrypt.
- Add your Sub-domain Name and other required information then submit request.
- Your SSL certificate and files are returned by Let's Encrypt and available in NGINX.
- Email address.
- Turn ON "Use a DNS Challenge".
- Select DNS Provider "DuckDNS".
- In "Credentials File Content" enter your DuckDNS token.
- Click on "I Agree to the Let's Encrypt Terms of Service".
- Click on "Save".
- cert1.pem
- chain1.pem
- fullchain1.pem
- privkey1.pem
- Dynamic DNS (setup for a constantly changing IP address) and associated DuckDNS sub-domain
- An wildcard SSL certificate (for your DuckDNS sub-domain)
- NGINX installed
- The unique domain name that will be used to access our web server on the internet and using our sub-domain of duckdns 'snowwhiteforeva'
- Scheme: https
- IP address: 192.168.1.111 (in our example that is the IP of the development PC running the NodeJS web server)
- Port: 8100 (the IP the web server is using)
- Select the wildcard SSL Certificate you created for *.snowwhiteforeva.duckdns.org
- Click on 'Force SSL'
- Click on 'Save'
Step 1: Create project folder
Step 2: Initialize a Node.js project
In the Terminal run code: npm init -y
Step 3: Install Middleware dependencies
In the Terminal run code: npm install express.
This installs the 'Express' middleware, a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
Step 4: In the project folder create a new file called 'server.js'
Step 5: Code simple NodeJS web server
Write the code for the NodeJS web server in Visual Studio Code noting that in this example the server will be running on port: 8100 and on the development computer with IP address: 192.168.1.111.
const express = require('express');
const app = express();
const PORT = 8100;
app.get('/', (req, res) => {
res.json({ message: 'Hello from API on port 8100!' });
});
app.listen(8100, '192.168.1.111', () => {
console.log('API running on http://192.168.1.111:8100');
});
In Visual Studio Code, it will look like this:
The program can be broken down as follows:
const express = require('express');
- Loads the Express.js library.
- Express simplifies building HTTP servers in Node.js — handling routes, middleware, JSON responses, etc.
const app = express();
- Creates an instance of an Express application.
- This app object is what you'll use to define routes and middleware.
const PORT = 8100;
- Just a constant for the port number the server will listen on.
- Used later in app.listen().
app.get('/', (req, res) => { ... });
- Defines a GET route for the path / (the homepage).
- When someone visits http://192.168.1.111:8100/, this function is called.
- Inside the callback it sends a JSON response like
{ "message": "Hello from API on port 8100!" }
app.listen(8100, '192.168.1.111', () => { ... });
- Starts the server and listens for connections on:
- Port 8100
- IP address 192.168.1.111
This binds the server only to that specific IP (not to all interfaces).
The callback logs this message to confirm:
API running on http://192.168.1.111:8100
Step 6: Start the NodeJS web server
In the Terminal type: node server.js to start the web server.
In the terminal window you will get a message that the API is running.
Step 7: Test the NodeJS web server is running
In your web browser on the development computer type: : node server.js to start the web server.
Congrats! You've successfully coded a super simple NodeJS web server.
Nginx
We will be using Nginx in this tutorial for both:
Pre-requisite: You have Nginx installed on Unraid. For installation instructions if you do not have Nginx installed follow this tutorial.
This tutorial covers the required port forwarding (on your router) and gets us to the point where we can add a "proxy host" in Nginx. Adding a "proxy host" is required later in this tutorial.
What is a proxy host: In very simple terms, a proxy host acts as an intermediary server that receives client requests and forwards them to one or more backend servers, essentially acting as a "middleman". This is commonly implemented as a reverse proxy, where Nginx sits in front of the backend servers, hiding their details from the client.
Dynamic DNS and Catering for Non-static IP address
If you are a home user often your premises does not have a static IP address and your internet service provider is in the background updating your IP address in the background without you ever knowing. This probably isn't an issue from a day to day operational point of view.
Background: A dynamic (non-constant) IP address becomes an issue when we want to make our data available over the internet using a domain name, eg, google.com or apple.com. The domain name is using what is known as the DNS (Domain Name System) that translates human readable domain names (like google.com) into numerical IP addresses that computers use to identify each other on the internet.
The problem: If your IP address changes, and you're using a domain name, anyone trying to reach your device using that domain name will fail to connect because the DNS record is no longer accurate.
Solution: Dynamic DNS (DDNS) saves the day! A DDNS service acts as a bridge, constantly monitoring your device's IP address and updating the DNS records accordingly. This ensures that your domain name always points to the correct, current IP address, even if it changes frequently.
Pre-requisite: In this tutorial you have to have:
DuckDNS Account: In this example the user has a token as per the image below:
In our example, the sub-domain (of domain "duckdns.org") is "snowwhiteforeva". This has an associated token provided to you from DuckDNS.
Request SSL Certificate
We utilize NGINX to request an SSL Certificate from LetsEncrypt for our DuckDNS sub-domain. There are a few benefits of this approach:
High Level Process - SSL Certicate Request
Step by Step Process
Step 1: Open NGINX.
Step 2: Click on SSL Certificates --> Add SSL Certificate --> Let's Encrypt.
Step 3: Add your Sub-domain Name
Enter your DuckDNS SubDomain Name with a prefix of *. and click on Add.
DuckDNS token: Have the token for your DuckDNS subdomain ready as this is required to get your SSL certificate.
Then enter:
Step 4: A SSL certificate will be created by Let's Encrypt. As the SSL certificate isn't yet being used it has a status of "Inactive".
Mission accomplished! You have a wildcard SSL certificate for your DuckDNS sub-domain.
Please support this channel: Have I saved you minutes, hours or even days of scouring the internet to find an actual working solution.
It takes me time and effort to both find a working solution and then write everything up. Please consider buying me a coffee so I can keep producing useful content, especially if I've made your life easier. Cheers!
Start using your SSL certificate: As the certificate was created using NGINX you can now use the certificate you created to add a Proxy Host.
Conversely, you can download the SSL certificate files if you are using NGINX as a tool to request the SSL certificates to be used elsewhere.
Downloaded SSL certificate files: At the time this post was written the downloaded zip files contains the following:
NGINX: Add Proxy Host
It's taken quite the journey to get here but now you have the building blocks to create the "proxy host" for your web server.
The pieces you've put together are summarized:
Step 1: In NGINX click on the "Add Proxy Host" command button.
Fill in the form as per below noting:
Using this information it means that our web server will be available across the internet using the URL: test.snowwhiteforeva.duckdns.org.
Step 2: Add an SSL certificate: Under 'Edit proxy Host' click on 'SSL' then do the following (see image below):
Congratulations! You've created a "Proxy Host" in NGINX Proxy Manager.
Add Cross-Origin-Resource-Sharing (Cors)
If you want the data accessible to websites that are not on your own domain then you will have to add Cross-Origin-Resource-Sharing (Cors) support. If not, the chances are that the webpage that is used to access your web server will be rejected automatically by the web browser. This is a modern security feature that you cannot dodge.
As an aside you maybe thinking why does the NodeJS application not have any code to apply Cors. In this example we are about to pass all the Cors logic to NGINX proxy Manager. You could apply Cors in your NodeJS application instead but that is not this tutorial.
Now the next steps are arguably the most difficult in this tutorial. Cors is a fiddly and challenging security feature to comply with. NGINX Proxy Manager simplifies things for the novice or infrequent user for basic configuration. However, applying Cors through the gui is a fully manual process.
WARNING: Anyway, as a way of warning do not try and apply Cors headers by manually editing the proxy host configuration files. While you can save them temporarily, NGINX Proxy Manager will automatically over-write them.
Step 1: Edit Proxy Host Configuration
Click on 'Edit' so we can change add additional settings to the Proxy Host configuration file.
Step 2: Advanced Settings
In the pop-up form switch from 'Details' to the 'Advanced' tab near the top right of the form. We will be adding our Cors settings in the section labeled 'Custom Nginx Configuration'.
Step 3: Add Cors custom settings
In the 'Custom Nginx Configuration' text box paste in the following code and click 'Save':
location / {
# Handle preflight (OPTIONS) directly
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
# Apply CORS headers to all other requests
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# Proxy! to Node.js backend
include conf.d/include/proxy.conf;
}
These custom configuration settings can be broken down into three main components:
1. Protocol setting (that gets saved into the {server} block of the proxy host configuration file;
2. # Handle preflight (OPTIONS) directly (that gets saved into the {location} block)
3. # Apply CORS headers to all other requests (that gets saved into the {location} block)
Explanation of the Code: Protocol Setting
set $forward_scheme http;: This line sets a variable named $forward_scheme to the value http. This variable is then used in the proxy_pass directive to determine the scheme (protocol) used for forwarding the request.
In Nginx Proxy Manager, the command set $forward_scheme http; within a proxy host file sets the scheme for forwarding requests to the backend server to http. This means that even if the client connects to the proxy via HTTPS, the proxy will forward the request to the backend server using HTTP. This is useful when the backend server only supports HTTP or when you want to handle SSL termination at the proxy level.
Example: If you have a website accessible via https://example.com, and you configure Nginx Proxy Manager to forward requests to a backend server on port 80 (HTTP) with this configuration, the request will be forwarded as http://backend_server:80.
Purpose: This configuration is common when you want to handle SSL termination at the proxy level. The client connects to the proxy via HTTPS, the proxy decrypts the traffic, and then forwards the unencrypted HTTP traffic to the backend server. This simplifies the backend server's configuration, as it doesn't need to handle SSL/TLS certificates and encryption itself.
This is the case in this tutorial where NGINX is doing the heavy lifting regards SSL, etc and the web server is as basic as it gets.
Alternative: If you want to forward requests with HTTPS to the backend server, you would change the set command to:
set $forward_scheme https;
and ensure the backend server is configured to accept HTTPS connections. That lesson is for another day.
Explanation of the Code: # Handle preflight (OPTIONS) directly
# Handle preflight (OPTIONS) directly
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
}
This section of code handles CORS preflight (OPTIONS) requests.
if ($request_method = OPTIONS) { ... }
This checks if the incoming HTTP request is an OPTIONS request.
Browsers send this automatically as a preflight request before sending certain types of cross-origin requests.
add_header 'Access-Control-Allow-Origin' 'http://www.50plusjourney.com' always;
Tells the browser: "Yes, this server allows requests from http://www.50plusjourney.com."
Critical for CORS.
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
Tells the browser which HTTP methods are allowed for the actual cross-origin request.
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
Specifies what custom headers the browser is allowed to send in the real request.
add_header 'Access-Control-Allow-Credentials' 'true' always;
Allows cookies and HTTP auth headers (like Authorization) to be sent in cross-origin requests.
add_header 'Access-Control-Max-Age' 86400 always;
Tells the browser to cache the preflight result for 24 hours (86400 seconds), so it doesn't need to resend OPTIONS every time.
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
Defines the content type of the response — plain text with UTF-8 encoding.
add_header 'Content-Length' 0 always;
Explicitly sets the response body length to 0.
return 204;
Responds with HTTP 204 No Content.
This tells the browser "OK, you're clear to send your real request."
No body, no errors, just silent approval.
This block is for | Purpose |
---|---|
Preflight (OPTIONS) requests | Tells the browser it's okay to make the real request |
Avoids hitting your backend | Saves resources and avoids unnecessary proxying |
Ensures valid CORS headers are returned | Prevents CORS errors in browser |
Explanation of the Code: # Apply CORS headers to all other requests
add_header 'Access-Control-Allow-Origin' 'https://www.50plusjourney.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
}
This section of code applies CORS headers to normal (non-OPTIONS) requests, such as GET, POST, PUT, or DELETE.
add_header 'Access-Control-Allow-Origin' 'https://www.50plusjourney.com' always;
Tells the browser that this server allows requests from the origin http://www.50plusjourney.com.
Required for cross-origin requests to be permitted.>
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
Declares which HTTP methods are allowed when the browser sends a cross-origin request.
Helps both preflight and normal requests know what’s allowed.
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
Specifies which custom headers can be sent in the request.
This is necessary when your frontend sends headers like Authorization or Content-Type.
add_header 'Access-Control-Allow-Credentials' 'true' always;
Allows the browser to send cookies, session tokens, or Authorization headers in the request.
Must be set if you use credentials: 'include' in fetch() or AJAX.
Extra Learnings: Why the always keyword?
Ensures the header is added even when NGINX returns errors (e.g., 4xx or 5xx).
Without always, NGINX might omit CORS headers on error responses - which breaks browser behavior.
Extra Learnings: View Proxy Host Configuration Files
WARNING:
NGINX Proxy Manager will over-write your changes even if you 'Save' them when you edit the Proxy Host in the gui or shutdown and restart NGINX Proxy Manager. Viewing the Proxy Host Configuration files as per this extra learning is so you can better see the format, contents and settings that are otherwise hidden from you.
Testing: It is not for changing settings "permanently". It can however be useful for "testing" settings as any changes are not kept permanently.
Step 1: Find Proxy Host Configuration Files
Remembering that in this tutorial we are running NGINX Proxy Manager on Unraid, navigate your Unraid server and find the location of the Proxy Host configuration files that you generate via the NGINX Proxy Manager gui. In this example, the Proxy Host configuration files are located in this folder (see image below):
Each of the files in this folder represent a different Proxy Host that are added in the NGINX Proxy Manager front end.
Step 2: Find and Open the Configuration File for the Proxy Host created earlier in this Tutorial
The contents of your proxy host configuration file will look similar to this (see image below). Note the image is a subset and the file is a lot longer than that shown.
Step 3: Modify Server Block
In the proxy host configuration file find the 'Server' block and add the line:
set $forward_scheme http;
Web Page For Testing
A web page has to be designed to test access to the web server and that it complies with Cors.
In this tutorial we have applied strict access to the web server with requests only accepted from the domain:
- https://www.50plusjourney.com
WARNING: To test that the web server is working you have to test the request from a computer running on a network separate to where the web server is running.
This means that if you are testing from a home environment either test from a mobile phone that is using it's own data (not a wireless connection to your home network) or from a computer wirelessly connected to your mobile phone via a hotspot (again using the phone's data plan and NOT connected to your home network via wifi.
Create a web page on your website that uses the domain you have designated with the response header set using 'Access-Control-Allow-Origin' and set to accept 'https://www.50plusjourney.com'
<head>
<meta charset="UTF-8">
<title>API 8100 Test</title>
</head>
<body>
<h1>Test API Call to Port 8100</h1>
<button onclick="testAPI()">Call API</button>
<pre id="result"></pre>
<script>
async function testAPI() {
try {
const res = await fetch('https://test.snowwhiteforeva.duckdns.org/', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include' // or 'same-origin' if cookies are same-origin
});
const data = await res.json();
document.getElementById('result').textContent = JSON.stringify(data, null, 2);
} catch (err) {
document.getElementById('result').textContent = 'Error: ' + err.message;
}
}
</script>
</body>
</html>
After you have saved the web page, open it and it will render similar to this:
Click on the 'Call API' and if everything is working correctly the text box will be populated as per image below:
Congratulations
Congratulations - You've completed one mammoth learning exercise. This tutorial has a bit of everything so if you have got it working, well done.
Please support this channel: Have I saved you minutes, hours or even days of scouring the internet and youTube to find an actual working solution.
It takes me time and effort to both find a working solution and then write everything up. Please consider buying me a coffee so I can keep producing useful content, especially if I've made your life easier. Cheers!
Believe it or not a coffee goes a long way so if I've helped you out a coffee would be great. Cheers!
0 Comments