Raphael Mun
Raphael Mun is a tech entrepreneur and educator who has been developing software professionally for over 20 years. He currently runs Lemmino, Inc and teaches and entertains through his Instafluff livestreams on Twitch building open source projects with his community.

Socket.IO is easy to use and great for real-time communication between apps, but correctly configuring a cloud server to use a secure SSL-encrypted connection without performance degradation can be tricky. If you’ve run into such performance issues, messed with Apache configuration and proxying ports for Socket.IO handshakes and WebSocket traffic, or tried to get Let’s Encrypt to actually auto-renew properly so you’re not logging in every three months to manually run your renew script, then you’re in luck because you’ve finally found the guide that will actually work, written by someone who’s been there. Making sure WebSocket connections to backends are both securely encrypted over WSS and fast shouldn’t have to be so frustrating.

For this guide, we’ll use the following technologies (but you can use whichever platform or service you prefer):

  • Cloud: AWS Amazon Lightsail
  • Domain: Namecheap
  • SSL: Let’s Encrypt

The Socket.IO web app sample used in this guide is available on GitHub at https://www.github.com/instafluff/SecureWebSockets. For a brief explanation of the code, see the "More on the Code" section at the end of the article.

Why WebSockets and Socket.IO?

Socket.IO is a real-time, event-based messaging library that’s much faster than normal API calls done over the network using HTTP. If HTTP requests are like snail mail, WebSockets are like phone calls, which means they’re ideal for games, news and stock tickers, and other low-latency/fast-network scenarios. Socket.IO is one of the most popular libraries out there, making it simple to establish this communication channel between apps.

So why not use it all the time? WebSockets maintains the connection between you and the server, so it’s more CPU- and network-intensive, and therefore more costly to scale.

Performance Concerns

When running on an HTTP server like Apache or Nginx, it’s possible to proxy the WebSocket traffic to your Node server, but that dramatically slows your server’s ability to handle socket connections. Instead, your web app should connect directly to your backend to be able to handle as many users as possible.

Setting Up Your Cloud

In this guide we’ll use a custom domain with a Linux-based instance in Amazon Lightsail. So create your domain (I used Namecheap), then create an AWS account if you haven’t already at https://aws.amazon.com/lightsail/. Then log in to the Lightsail dashboard at https://lightsail.aws.amazon.com/ls/webapp/home/resources and create an instance:

Select Linux for the instance image and Node.js for the blueprint. You can use the defaults for the remaining options.

To get the first month free, select the lowest price plan, then name your instance and click Create instance:

After a few minutes, your server will be ready.

Next, we’ll direct the custom domain to our Lightsail instance. First, log into your registrar and get to the DNS settings:

Copy the IP address of your Lightsail instance, add an A Record entry to your custom domain, and set a subdomain—mine is socketexample.instafluff.tv.

Make sure the domain/subdomain correctly points to your new instance by opening up your domain in a web browser. This can take anywhere between a few minutes to (hopefully not) a whole day.

Securing Your Server with SSL Certs

The next step is to generate an SSL certificate using Let’s Encrypt. First, connect to your Lightsail instance to get a terminal window:

Now install the Let’s Encrypt certbot by running the following commands:

sudo apt-get update`
sudo add-apt-repository ppa:certbot/certbot`
sudo apt-get update`
sudo apt-get install certbot`

To get a certificate for our custom domain, we’ll take advantage of Apache, which is already set up on this image, then turn it off. First, install the Apache certbot plugin:

`sudo apt-get install python-certbot-apache`

Then create a symlink for apache2ctl to apachectl:

`sudo ln -s /opt/bitnami/apache2/bin/apachectl /opt/bitnami/apache2/bin/apache2ctl`

Now run `sudo certbot –apache` and enter your email and your custom domain without the www or http (for example, socketexample.instafluff.tv). When you’re asked to choose whether to redirect HTTP traffic, select 1: No redirect as we won’t be using Apache for our server.

When you’re done, disable Apache for good using the following commands (which apply only to Bitnami images):

`sudo /opt/bitnami/ctlscript.sh stop apache`
`sudo mv /opt/bitnami/apache2/scripts/ctl.sh /opt/bitnami/apache2/scripts/ctl.sh.disabled`

Installing Your Web App

We’re ready to run the sample server application. To do this, clone the sample code repository and install the dependencies, by running the following commands:

  1. `git clone https://www.github.com/instafluff/SecureWebSockets.git`
  2. `cd SecureWebSockets`
  3. `npm install`

Next, create a .env file by opening the nano editor with `nano .env`. Add the following lines, then save by pressing Ctrl+O.


Finally, test run the app using `sudo node index.js`. Then open "https://your.customdomain.com" in your computer’s web browser. If all went well, you should see a page with a numbered button that counts up as you click:

Bonus: Run Your App Worry-Free with a Process Manager

To finish up, let’s dot our i’s and cross our t’s to make sure our server requires only minimal maintenance. We’ll use the Forever command-line tool to run the Node server in the background so we can log off the console with the site still running. Run the following commands:

`sudo npm install -g forever`
`sudo forever start index.js`

If you need to restart the app, run:

`sudo forever restartall`

To keep your server running for longer than three months without having to think about it, follow these steps to automatically renew your Let’s Encrypt certificates:

  1. Add a scheduled task to your crontab file by running `sudo crontab -e` with nano.
  2. Add the following line to the bottom of the file:

0 0 * * * /usr/bin/certbot renew –pre-hook "sudo forever stopall" –post-hook "sudo /opt/bitnami/apache2/bin/apache2ctl stop && sudo forever start –sourceDir /home/bitnami/SecureWebSockets index.js"

  1. Press Ctrl+O and Ctrl+X to save the file and exit.

This task will run every day at midnight and, when your certificate is up for renewal, the hooks will stop your server, renew with Let’s Encrypt using the Apache plugin, stop Apache, and, finally, restart your server.

Ensure HTTP Traffic is Redirected to Secure HTTPS

Last but not least, to make sure normal HTTP requests are automatically redirected to the HTTPS equivalent instead of going nowhere, simply uncomment the bottom part of your index.js file to start a port 80 server that will redirect to the secure 443 port:

Note:Remember to run `sudo forever restartall` to restart your server afterwards!

Wrapping Up

This article showed you how to set up a Linux Amazon Lightsail server and configure a NodeJS application to use SSL certificates for a custom domain via Let’s Encrypt. We demonstrated this with an open-source, real-time Socket.IO client-server connection, using sample code built for this article that’s hosted on GitHub.

The sample project creates a web server with HTTPS, if certificates exist, to serve an index.html file and open a WebSocket connection to handle click events. Each click event will increment a counter and the counter total to all pages connected will then be broadcast.

You should be good to go, so try this: Open two different instances of the web page and then click the button on one page to see it update in real-time on both!

More on the Code

For a bit more information on how the code works, let’s take a quick look at a few lines in the index.js and index.html files. I’ve included them below as both text and images.

– index.js –

require( "dotenv" ).config();

const fs = require( "fs" );
const port = process.env.PORT || 3000;

function setupServer() {
	let indexPage = fs.readFileSync( "index.html", "utf8" );
	indexPage = indexPage.replace( "CUSTOMDOMAIN", process.env.DOMAIN || `http://localhost:${port}` );
	if( process.env.CERTIFICATE && process.env.CERTCHAIN && process.env.PRIVATEKEY ) {
		// Create a server with the certificates
		return require( "https" ).createServer( {
			key: fs.readFileSync( process.env.PRIVATEKEY, "utf8" ),
			cert: fs.readFileSync( process.env.CERTIFICATE, "utf8" ),
			ca: fs.readFileSync( process.env.CERTCHAIN, "utf8" )
		}, ( req, res ) => res.end( indexPage ) );
	else {
		return require( "http" ).createServer( ( req, res ) => res.end( indexPage ) );

let numClicks = 0;
let server = setupServer();
let io = require( "socket.io" )( server );
io.on( "connection", conn => {
	conn.emit( "count", numClicks ); // Send latest click count on connect
	conn.on( "click", () => {
		console.log( `Clicks: ${numClicks}` );
		io.emit( "count", numClicks ); // Broadcast new click count
	} );
} );

server.listen( port, ( err ) => {
	if( err ) {
		return console.error( 'Server could not start:', err );
	console.log( "Server is running" );
} );

// Uncomment the following code to redirect HTTP traffic to HTTPS
// const httpServer = require( "http" ).createServer( ( req, res ) => {
// 	res.writeHead( 301, { Location: `https://${req.headers.host}${req.url}` } );
// 	res.end();
// } );
// httpServer.listen( 80 );

Line 1: require( "dotenv" ).config()

This sets up the dotenv module to read environment variables, such as DOMAIN, CERTIFICATE, PORT, and so forth, from a .env file.

Line 6: setupServer()

This function reads the index.html web page file, replacing the word CUSTOMDOMAIN with the actual localhost or the custom domain name, and finally returns either an HTTPS or HTTP web server, depending whether the .env file specifies CERTIFICATE, CERTCHAIN, and PRIVATEKEY.

Line 25: Socket.IO

The Socket.IO connection handler acknowledges new connections to the server and sends the latest click count, and upon click events from this connection, increments the internal counter and broadcasts to all connections using io.emit() instead of conn.emit().

Line 34: server.listen()

This line starts the web server on the defined port, which should be 443 for standard SSL connections, or it should default to 3000.

– index.html –

<!DOCTYPE html>
    <title>Secure Socket Demo</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
    <button id="clicker" onclick="addClick()"></button>
      var socket = io( "CUSTOMDOMAIN" );
      socket.on( "count", ( count ) => {
        document.querySelector( "#clicker" ).innerText = `Total Clicks: ${count}`;
      } );
      function addClick() {
        socket.emit( "click" );

Line 10: Socket.IO

This line opens a connection from this webpage to the server ("CUSTOMDOMAIN" is replaced from the server-side) and updates the button’s text to the latest click count.

Line 14: Click Event

This line sends a click event via Socket.IO in real-time to the connected server.

How to work with us

  • Contact us to set up a call.
  • We will analyze your needs and recommend a content contract solution.
  • Sign on with ContentLab.
  • We deliver topic-curated, deeply technical content to you.

To get started, complete the form to the right to schedule a call with us.

Send this to a friend