I come across ngrok more and more every day; it provides an excellent HTTP tunnel from my local computer to the internet. I use it to debug Slack apps locally, so I would like to integrate it with my Node.js application.

The free version generates a new sub domain every time you connect, that’s why I’m usually start ngrok at the beginning of my day. I’ve tried to use the ngrok npm package in my application, but as the documentation says: “The ngrok and all tunnels will be killed when node process is done.” I need the process to “survive” my application. Let’s see what we can do about that…

Process

One picture says more than a 1000 words, so I created a flow-diagram to show what we are trying to do:

Diagram for starting NGROK in detached mode.
Diagram for starting NGROK in detached mode.

We will start the ngrok process in detached mode, so it will survive the shutdown of our application. This is especially important, when you run a nodemon that restarts the application on every save.

Dependencies

I’m using 2 packages to make this work:

npm install axios --save-dev
npm install ngrok --save-dev

The ngrok package ships the ngrok binary, which will launch the ngrok tunnel. The axios package is used to retrieve the ngrok URL.

Configuration

Add a ngrok configuration yaml file named ngrok.yml to the root of your application and fill it like this:

tunnels:
  app:
    proto: http  # exposes an HTTP end point
    addr: 3000   # uses local port 3000

We also need to add an environment setting: NGROK=true. Production environments will not have this variable set, so ngrok is not started.

The script

This file lives in my root directory and is called ngrok.js.

// imports
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { platform } = require('os');
const { spawn } = require('child_process');

// settings
const pollInterval = 500;
const ngrokEnabled = process.env.NGROK || false;
const ngrokConfig = path.resolve('ngrok.yml');

// needed for spawning NGROK
let ngrokBin = '';
let ngrokDir = '';
try {
  const ext = platform() === 'win32' ? '.exe' : '';
  ngrokDir = path.dirname(require.resolve('ngrok')) + '/bin';
  ngrokBin = ngrokDir + '/ngrok' + ext;
}
catch { }

async function ensureConnection(callback) {

  if (!ngrokEnabled) return false;

  if (!fs.existsSync(ngrokConfig)) {
    console.log(`Can't run ngrok - missing ${ngrokConfig}.`);
    return false;
  }

  if (ngrokBin == '') {
    console.log("Can't run ngrok - are dev dependencies installed?");
    return false;
  }

  console.log("Ensuring ngrok...");
  const url = await connect();
  if (url == null) return false;

  callback(url);
  return true;
}

async function connect() {
  let url = await getNgrokUrl();
  if (url) {
    console.log("ngrok already running.");
    return url;
  }

  console.log("Starting ngrok...");
  await startProcess();

  while (true) {
    url = await getNgrokUrl();
    if (url) return url;
    await delay(pollInterval);
  }
}

async function getNgrokUrl() {
  const ping = 'http://127.0.0.1:4040/api/tunnels';
  try {
    const response = await axios.get(ping);
    return response.data.tunnels[0].public_url;
  }
  catch { return null; }
}

function startProcess() {
  const start = ['start', '-config=' + ngrokConfig, "app"];
  const proc = spawn(ngrokBin, start, { cwd: ngrokDir, detached: true });
  proc.unref();
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports.ensureConnection = ensureConnection;
module.exports.getNgrokUrl = getNgrokUrl;

Usage

Here is how I use it with a simple express server:

const express = require('express');
const { ensureConnection } = require('./ngrok');
const port = process.env.PORT || 3000;

const app = express();
const server = createServer(app);
server.listen(port, async () => {
  console.log(`Listening on port ${server.address().port}`);
  await ensureConnection(url => {
    console.log(`Listerning to ${url}`);
  });
});

Terminate ngrok background process

On Windows we can terminate ngrok like this:

Taskkill /IM ngrok.exe /F