The purpose of this guide is to help someone setup a secure websocket server on any Ubuntu VM (AWS/Linode/Digital Ocean/Azure etc.).


Given the current state of the world, supporting features such as collaborative editing and real-time chat became almost essential to TunePad's long term success.

All modern browsers support websockets and a lot of progress has been made since the time socket.io was the only option. The simplest way to get real-time chat up and running was to use a Node package called 'ws' to set up a websocket-server (written in Javascript, of course), and then write a client in any language we wish to (in our case, Dart).

The documentation – and it is excellent – for setting up a local client + server is already available in the 'ws' repository; however, I found that figuring out a production-level/cloud VM setup required collating multiple guides. In addition, if your site runs on an https connection, then the websocket also needs to be encrypted. Most browsers block mixed content (i.e. content from http and https sources is frowned upon), and in this case, the 'wss' protocol is preferred.

Without further ado, let's dive in.


Step 1: Install Node and the WS Package

Install Node/npm (node package manager)

sudo apt-get install nodejs npm #install node/npm
sudo ln -s /usr/bin/nodejs /usr/local/bin/node 

Then, create a directory on the VM where you wish to store/run the websocket. In my case, I made a directory called "dropin".

sudo mkdir dropin #creates a directory called dropin
sudo chown <user_name>:<group_name> dropin 
cd dropin
npm init #will create the requisite project.json file
npm install ws #installs the ws module/package
make sure the owner and group for this directory is set to the correct access level (basically execute access is important). E.g. in my case, group_name was www-data.

The "npm init" command will create a project.json file, you can breeze through it by pressing the return key, and then edit the generated file later. Here's a sample from my setup

the only things I changed were the description and the main/entry-point, the rest were the default values.
read the documentation on github for the ws package if you wish to know more about the warnings, but for our purposes, we do not need the bufferutil and the utf-8-validate modules.

Step 2: Decide on the websocket port & get/use SSL certificate

Choose a port for the websocket, as long as it is not one already in use by the system, you should be okay; my tip would be to select a port that is not commonly used. Say, in this case, the port is 56112; allow TCP connections:

#open socket port
sudo ufw allow 56112/tcp

In order to setup a secure socket, you need access to SSL certificates. You can get new ones using Certbot, or you can reuse the certificates that have already been issued for your domain. In either case, we need the path to the cert.pem and the key.pem files for the websocket server setup. If you used LetsEncrypt to generate the certificate for your domain, this is what you'll have to do:

sudo certbot certificates #lists all current certificates

#--- if you want a new domain/A-record certificate, run this---#
sudo certbot --domains your_domain_name
#------#

#in the path from the "certbot certificates" command, list the *.pem files (the default would look like this below)
sudo ls /etc/letsencrypt/live/your_domain_name

#jot down the names of the certificate (usually cert.pem) and the private key (usually key.pem or privkey.pem)

Now, in the editor of your choice, say Vim, Nano etc., paste the code for the server given below and save it (name the file the same as the main/entry-point name in the package.json file, i.e. the name we used after running npm init), in this case the filename is server.js

Server Code

const fs = require('fs');
const https = require('https');
const WebSocket = require('ws');

//CHANGE THE CERT FILE PATHS
const server = https.createServer({
  cert: fs.readFileSync('/etc/letsencrypt/live/your_domain/cert.pem'),
  key: fs.readFileSync('/etc/letsencrypt/live/your_domain/key.pem')
});
const wss = new WebSocket.Server({ server});

console.log("started web socket server...")

wss.on('open', function open() {
  console.log('connected');
  ws.send(Date.now());
});

wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {

    // sends the data to all connected clients
    wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(message);
        }
    });
  });
});

//Ensure that the port is not in use
server.listen(56112);
ensure that the certificate and the key file paths used in the server definition are correct. Save this file with the name that matches the one in the package.json "main" key.

Keeping fingers and toes crossed, you are now ready to run the websocket server and test it!

we are using sudo here because the certificates are stored in a location (/etc/letsencrypt/live/your_domain/) that a user wouldn't have even read access to. For security, do not change the permissions for those files, or copy them over somewhere else.

Now, let's test our setup via WebSocket.org. If all went well, you should be able to connect and send a message.

the location for the websocket would be wss://<your_domain>:56112 

Step 3: Running the websocket server via systemd

Now that we have successfully setup the websocket server, we need to make sure that it runs as a service (that means, you don't need to run "sudo node server.js" each time, and it starts up upon server reboot automatically).

To do that, we would use systemd. Here are the commands/setup:

cd /lib/systemd/system
sudo vim /lib/systemd/system/<service_name>.service
do not forget to change the <service_name> to something you like.

The command above would open up an empty file. Edit the contents of the file to be something similar to what I have here:

[Unit]
Description=Starts a secure web socket
Documentation= wss://<your_domain>:56112/
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/node <your_directory_name>/server.js
Restart=on-failure

[Install]
WantedBy=multi-user.target
change the <your_domain> and <your_directory_name> to match your setup.

Now, just try starting the service, and afterwards check its status to see if it worked.

sudo systemctl start dropin
sudo systemctl status dropin

You should see something similar to this:

now the websocket server is running as an OS level service, that will automatically restart when the server is restarted.

Websockets are really neat, with a little bit of front end code, you can now make a real-time chat client!

a simple chat client that uses a secure websocket server similar to the one we setup.