Skip to main content
Node js WS

This article basically describes how to create a node js websocket and secure it using let's encrypt. Because the wss differs from https, it's slightly tricky to do this directly on the node js server directly using self signed certificates which is why I'm using nginx in front of it. Otherwise you will have to manually load the cert and initialize the node js http server which is harder to do when using let's encrypt because they don't support node js servers as easily as they do for nginx, apache etc.

Firstly we must create a simple node js websocket server using ws. Here's how that looks:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 4433 }) ;

let connections = [];

wss.on('connection', (ws, request) => {
  // Store connections.
  connections.push(ws);
  ws.on('message', (data) => {
    // Send message to client when connected.
    ws.send("Connected to server!");
  });
});

You can test this from your local from your browser's console by typing let ws = new WebSocket('ws://localhost:4433'). You can also use the cli tool wscat.

Now we need to containerize this with an nginx reverse proxy to redirect websocket requests and also to handle let's encrypt certificate generation.

This is the docker-compose.yml:

version: "3.2"
services:
  node:
    build:
      context: './docker/node/'
    restart: always
    ports:
      - "4433:4433"
    volumes:
      - ./:/var/local/ws
    networks:
      - backend
    container_name: node_ws
  nginx:
    build:
      context: './docker/nginx/'
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./.cert:/etc/letsencrypt
    container_name: node_nginx
    networks:
      - backend
    depends_on:
      - node

networks:
  backend:

Now we need the context Dockerfile for both the images. Here's the one for the node container:

FROM node:17.3-alpine3.14

WORKDIR /var/local/ws
CMD ["node", "src/ws.mjs"]

And the one for nginx: 

FROM nginx:1.21.4-alpine

RUN apk update; \
    apk upgrade;
RUN apk add --no-cache certbot python3 python3-dev py3-pip build-base libressl-dev musl-dev libffi-dev && \
    pip3 install pip --upgrade && \
    pip3 install certbot-nginx;
COPY nginx.conf /etc/nginx/nginx.conf
RUN mkdir /var/www && mkdir /var/www/html && mkdir /var/www/html/web && touch /var/www/html/web/index.html
COPY ws.conf /etc/nginx/conf.d/default.conf
RUN echo '0 */12 * * * root certbot -q renew --nginx' > /etc/crontabs/root
CMD [ "sh", "-c", "crond -l 2 && nginx -g 'daemon off;'" ]

Now the nginx configs that are used in the nginx Dockerfile. 

# nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  768;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    map $http_upgrade $connection_upgrade {
       default upgrade;
       '' close;
    }

    upstream websocket {
      server node:4433;
    }

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 9;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 256;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript font/eot font/otf font/ttf application/x-javascript application/ld+json application/manifest+json application/rdf+xml application/rss+xml application/xhtml+xml  image/webp image/png image/jpeg image/svg image/svg+xml image/x-icon;

    include /etc/nginx/conf.d/*.conf;
}

The vhost conf for the websocket.

# default.conf
server {
    server_name ws.domain.com;
    root /var/www/html/web;
    location ^~ /.well-known/acme-challenge/ {
       log_not_found off;
       access_log off;
       allow all;
    }
    location /index.html {
       log_not_found off;
       access_log off;
       allow all;
    }
    location /ws {
       proxy_pass http://websocket;
       proxy_http_version 1.1;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection $connection_upgrade;
    }

}

After this is done, `docker-compose up` to spin up your containers. You should be able to connect to the websocket using the nginx port now: ws://localhost:80. To protect this using ssl, you will have to sh into the nginx container (`docker exec -it node_nginx sh`) and run `certbot --nginx`. Your default.conf file in the container at /etc/nginx/conf.d/ will be changed by certbot. Make sure to copy this into your ws.conf file so that you don't lose the ssl configs.

Now you have websocket that's using a let's encrypt certificate. Ask your questions in the comments and I'll be glad to respond to them.

x

Work

Therefore logo
80 Atlantic Ave, Toronto, ON Canada
Email: hello@therefore.ca
Call us: +1 4166405376
Linkedin

Let us know how we can help!