../ Debug nodeJS Remotely with Emacs

Lately at work I've had to deal with an application with a backend written using nodeJS. I wanted to understand what the application was doing by doing interactive debugging sessions. The only constraints I had were the following two:

  • I wanted to use emacs.

  • The application executed within a remote server that I could login through ssh.

After some searches and some configuration, I managed to find the right setup. Said in another way, I'm able to debug remotely an application written in nodeJS all while using emacs, which is used both for writing and editing new code as well as a debugging interface. In this blog post I will showcase my setup, and at the end I will link to some of the references and packages that I have used.

Have fun!

#Step 1 – Lab Setup

In order to showcase the setup I will first create a situation which simulates the scenario I was working with. In our scenario we have a remote server running a local nodeJS app. We will be able to connect to this remote server by using ssh. We will simulate the remote server with a docker container

So, as a first thing, let us start the docker service.

systemctl start docker

To build our container we use the following Dockerfile.

FROM ubuntu:latest

RUN apt-get update && apt-get install -y openssh-server curl nodejs passwd

RUN mkdir /var/run/sshd
RUN echo "root:password" | chpasswd
RUN sed -i 's/\#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

In this dockerfile we set a basic ssh account with login root:password. We also download nodejs. We can build the docker with docker build by executing the following command in the same folder where we have saved the Dockerfile.

sudo docker build -t nodejs-debug .

At this point we can start the docker by executing docker run

docker run -d -p 1337:22 --name test-nodejs-debug nodejs-debug

Notice the syntax for exposing port 1337. Continuing, with respect to the code we will debug, its the following, a simple "hello world" example.

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello World');
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

To test that everything works, we can first copy the code of interest with scp, and then we can login with ssh. Given that the server will listen in the localhost interface of the remote server, to interact with it we need to tunnel our connection through ssh. This can be done using the -L flag.

scp -P1337 app.js root@localhost:/root/
ssh -L 8000:localhost:3000 -p1337 root@localhost 

With the ssh session we can then start the application

node /root/app.js 

At this point if we go to our local browser at the URL http://localhost:8000 we should see the following

Let us see now how to setup a debuging configuration for this sort of scenarios.

#Step 2 – Debugging Configuration

In terms of emacs packages, for our purposes we need the following two: js2-mode, which is used to edit javascript code, and indium which will be our main emacs interface for debugging nodeJS code. To have a self-contained emacs configuration, one can use the following and start emacs with emacs -q -l emacs.el. The important thing is to remember to download use-package from github. Notice also the hook we define in the indium package configuration so that it is executed whenever we are editing JS code.

;; Added by Package.el.  This must come before configurations of
;; installed packages.  Don't delete this line.  If you don't want it,
;; just comment it out by adding a semicolon to the start of the line.
;; You may delete these explanatory comments.
(package-initialize)

;; NOTE: execute 'git pull https://github.com/jwiegley/use-package' from within the home directory of the user
(add-to-list 'load-path "~/use-package")
(require 'use-package)


(use-package package
  :config
  ;; Set priorities of using melpa-stable.
  ;; The higher the number, the higher the priority.
  (setq package-archive-priorities
	'(("melpa-stable" . 2)
	  ("MELPA" . 1)
	  ("gnu" . 0)))

  ;; Add to list melpa stable
  (setq package-archives
	`(("melpa-stable" . "https://stable.melpa.org/packages/")
	  ("MELPA" . "https://melpa.org/packages/")
	  ("gnu" . "https://elpa.gnu.org/packages/")
	  ))
  )

(use-package js2-mode
  :ensure t)

(use-package indium
  :ensure t
  :hook ((j2-mode . indium-interaction-mode))
  :config
  (setq indium-client-debug t))

The idea now is to use sshfs to mount the remote folder present in the server the app's running it with a local mount point in our machine. This is done for various purposes. For starters, it allows us to use emacs as we would use it to edit any other local file in our machine. The difference is that instead of only editing a local file, every change we make will be also pushed to its remote counterpart. This could also be done with packages such as tramp-term to connect remotely, however I have felt that sshfs is simply more efficient and easier to use.

NOTE: Given that my emacs runs as a normal user, I recommend do to this step with a normal user. In my case I will use leo, my non-root user.

mkdir remote
sshfs -p1337 root@localhost:/root remote 

First we create a folder called remote, and then we connect that folder with the /root folder present in the remote machine. At this point we can create a new file in ./remote/.indium.json. This file will be used by the indium package, and it contains information needed to start debugging sessions. The content of the file is as follows

{
    "configurations": [
	{
	    "name": "11ty",
	    "type": "node",
	    "host": "127.0.0.1",
	    "port": "8001",
	    "remoteRoot": "/root/"
	}
    ]
}

We have now everything we need to have to start debugging. Before doing that we can stop the application from running if we have still it open. We can also close the tunnel we created previously.

#Step 3 – Let's debug!

For starters, we will login with ssh again, but this time we will create two tunnels, one for transfering the usual application data, and another one for transfering the debugging data.

ssh -L 8000:localhost:3000 -L 8001:localhost:9229 -p1337 root@localhost 

With the ssh connection just created we can now start the application. This time, however, we will start it with the flag --inspect, which will also start the debugger on the local port 9229.

	 node --inspect /root/app.js
Debugger listening on ws://127.0.0.1:9229/b719ba41-eb4d-4421-9b1a-65b781be4143
For help, see: https://nodejs.org/en/docs/inspector
Server running at http://127.0.0.1:3000/

At this point we can test for basic connectivity with curl. Note however that this must be done in our local machine, and not in the shell we have just opened with the server.

curl localhost:8000 

We are finally read to debug the app with emacs. Indeed, we can then open the application file app.js with emacs (C-x C-f), start indium-interactive-mode and connect to the remote server using inidum-connect.

Now we can freely move around our file and we can add our breakpoints using the indium-add-breakpoint function.

By interacting with the webapp we can trigger such breakpoint and start to navigate the dynamic nodeJS environment as we please.

After we're done with our remote session we can call indium-quit, we can unmount the remote path with umount remote, and we can go on about our day.

#Step 4 – Conlcusion and Refs

At first I did not know if it was possible to do exactly this thing in emacs. Not because it could not technically be done, of course, given that you can do anything you want with emacs. I just wondered if there were already some packages and some guides that made this possible. And yes, there were, luckily for me!

The setup just showcased was made possible also because of the following resources. I thank every author for their contribution, and for making emacs usable in every possible scenario, such as the one just described.