
Expose Your Local Dockerized Node Server to the Web with Ngrok
Run your node server locally with Docker & Ngrok while enabling to communicate with 3rd party services and other devices
Tuesday, Dec 15, 2020
The problem
Let's say you're working on your local development server and encounter the need to test your environment from a different machine other than the one your working on which... is also on a different network.
Or that your local server is dependent on a 3rd party service and that API needs a longer time to process and send results back to your server via a callback interface.
The list can go on.
Trying to expose your local server running on http://localhost:${PORT}
to the public internet can be a pain.
Local tunneling services such as ngrok allow you to expose your local server to the public internet in a secure fashion.
The Solution ~ What we will build
We'll be using:
- Docker to containerize our server environment.
- In the docker config, install ngrok and the dependencies for our node server.
- Then we'll be using the ngrok npm package to create a connection in the application and get the forwarded DNS address to be accessible to the web.
- We can then pass that address/url to the other API's and recieve the results back on our local machine.
Project Setup (If starting from scratch)
Create project Directory
mkdir docker-node-server
cd or re-open vscode new directory as project root
cd docker-node-server
Create package.json
npm init -y
Install Dependencies
npm i express
Install Development Dependencies
npm i -D ngrok
Docker Setup
Create in the project root
Dockerfile
FROM node:12.18.1
# Install ngrok https://ngrok.com/download
RUN curl -s -O https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip \
&& unzip ngrok-stable-linux-amd64.zip \
&& mv ngrok /usr/local/bin/ \
&& rm -f ngrok-stable-linux-amd64.zip
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV PORT=8080
ENV NODE_ENV=development
ENV LOCAL_DEV=true
# ENV callbackUrl= [Prod URL]
EXPOSE 8080
CMD [ "npm", "start" ]
Brief DockerFile rundown,
- We start from a node image of
12.18.1
- Install ngrok within the container
- Create our working directory and install our npm modules (as if setting up our project from scratch)
- Set our environment variables we can access in the node runtime via
process.env.*
- Expose the docker container to be accessible via Port
8080
. - Finally start the node server
Create in the project root
.dockerignore
node_modules
The .dockerIgnore behaves as a .gitignore. Since we install our npm modules each time we build our Docker image, we want to ignore the existing modules from our image. Which will greatly reduce the size of the image as well.
Create our application server
index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 8080; // set via DockerFile ENV
const NODE_ENV = process.env.NODE_ENV || 'development'; // set via DockerFile ENV
const LOCAL = process.env.LOCAL_DEV || false; // set via DockerFile ENV
let callbackUrl = process.env.callbackUrl; // set via DockerFile ENV
// if running locally, set the callbackUrl to one assigned via ngrok
if (LOCAL) {
(async () => {
callbackUrl = await require('./exposeServer')(PORT)
console.log(`Server available at ${callbackUrl}`);
})();
}
app.use(express.json());
app.use(express.urlencoded());
app.get('/', (req, res) => {
res.send('Hello World!')
});
app.post('/results', (req, res) => {
// process results here
});
app.listen(PORT, () => {
console.log(`Example app listening at http://localhost:${PORT}`)
});
Basic boilerplate node/express server, if the process.env.LOCAL
environment variable is set, we require ngrok helper module mentioned below and use the ngrok service to expose the application to the web and assign a dynamic DNS record.
Helper function to expose our server
exposeServer.js
const ngrok = require('ngrok');
/**
* Should only be used for local development
* @param {*} port
*/
async function exposeLocalServer(PORT) {
let localCallback;
try {
localCallback = await ngrok.connect({ addr: PORT })
} catch(err) {
console.error(err);
}
return localCallback;
}
module.exports = exposeLocalServer;
This is a helper async function that requires the ngrok
npm package that gives us a javascript based API to interact with ngrok's services. We give it a port, and it will assign a DNS that any computer on any network can access. This will only work if the ngrok executable is installed on the machine (Installed as part of our Docker Image)
Update package.json Scripts
package.json
{
"scripts": {
"start": "node index.js",
"docker": "npm run docker-clean && npm run docker-build && npm run docker-run",
"docker-build": "docker build --no-cache --tag node-server .",
"docker-run": "docker run --name node-server-api node-server",
"docker-clean": "docker stop node-server-api || true && docker image prune -f && docker container prune -f"
}
}
*Since we opted to not use docker-compose
, building and starting the docker image/container can get tiring to type in manually. Just run npm run docker
, which will remove the old image, build the new updated image and finally run the container.
Test access to the Dockerized Node Server
Go ahead and run npm run docker
After the image is built and the container has started, should see the following logged to the console.
Example app listening at http://localhost:8080
Server available at https://6ccc57d4060b.ngrok.io
Go to your browser on another device and navigate to https://*.ngrok.io
and should receive back a response of Hello World!
Work with a 3rd Party API Callback
Since your node application has access to this ngrok address in the application layer, it can pass this information to an API/Service.
This scenario is usually necessary for requests that take a long time to process, and requires setting a callback endpoint of where to send the results back to.
Unfortunately, not aware of any public API's out there that have a callback interface to send results back to but here's a bit of pseudo-code.
...
const fetch = require('isomorphic-unfetch') // npm i isomorphic-unfetch
let data = [{...}];
let callbackUrl; // https://6ccc57d4060b.ngrok.io
const req = fetch('https://example.com/processdata/',
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({
data,
// where 3rd party will send results to
callback: `${callbackUrl}/results`
})
})
// handling post request from 3rd party
app.post('/results', (req, res) => {
// process results here
});
Conclusion
This is just one way to run your environment locally if this was a barrier to do so previously. You do not need to use docker with ngrok, but if you're familiar with docker it's an added convenience. Hopefully, this helps you as it has helped me. Cheers!