Websocket / API REST docker

Hello everyone, I have a routing problem between my docker which contains a websocket service and my docker traefik.

Objective: I want my websocket service to be accessible from my docker traefik in WSS via my domain name monsupertestdocker.com.

How add websocket(WSS) in my prod with traefik ?

Thank you

Here's the initialization of my nodeJS containing two services:

  • API REST which is on the '/api' prefix via port 3000
  • Websocket via port 3000

My server.ts file :

import express, { Express } from 'express';
import { AddressInfo } from 'net';
import { Config } from './config';
import { ContainerMiddleware } from './container-ioc';
import { Router } from './app/router';
import { Server as HttpServer } from 'http';
import WebSocket, { WebSocketServer } from 'ws';

export class Server {
  private config: Config;
  private express: Express;
  httpServer?: HttpServer;
  wss?: WebSocketServer;

  constructor({
    config,
    router,
    containerMiddleware,
  }: {
    config: Config;
    router: Router;
    containerMiddleware: ContainerMiddleware;
  }) {
    this.config = config;
    this.express = express();
    this.express.disable('x-powered-by');
    this.express.use(containerMiddleware);
    this.express.use(router);
  }

  start() {
    return new Promise<HttpServer>((resolve) => {
      const http = this.express.listen(this.config.port, () => {
        const { port } = http.address() as AddressInfo;
        console.log(`App listening on the port: ${port}`);
        this.setupWebSocket();
        resolve(http);
      });
      this.httpServer = http;
    });
  }

  setupWebSocket() {
    this.wss = new WebSocketServer({ server: this.httpServer });

    this.wss.on('connection', (ws: WebSocket) => {
      console.log('New client connected');

      ws.on('message', (message: string) => {
        console.log(`Received message => ${message}`);
      });

      ws.on('close', () => {
        console.log('Client disconnected');
      });
    });
  }

  broadcast(message: string) {
    this.wss?.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  }
}

With docker compose I create a container called my-backend that contains the API REST service and the websocket service, and another docker that contains the traefik that will do the routing between the 2 services.

My docker-compose.yml :

################################################
# NETWORKS
################################################
networks:
  my-network:
    external: true

################################################
# SERVICES
################################################
services:
  my-backend:
    container_name: my-backend
    image: registry.gitlab.com/dev9865655/my/my-backend:latest
    ports:
      - '3000:3000'
    networks:
      - my-network
    labels:
      # API REST
      - 'traefik.http.routers.my-backend-api.rule=Host(`monsupertestdocker.com`) && PathPrefix(`/api`)'
      - 'traefik.http.routers.my-backend-api.service=my-backend-api'
      - 'traefik.http.services.my-backend-api.loadbalancer.server.port=3000'
      - 'traefik.http.routers.my-backend-api.tls=true'
      - 'traefik.http.routers.my-backend-api.tls.certresolver=myresolver'
      # WebSocket
      - 'traefik.http.routers.my-backend-ws.rule=Host(`monsupertestdocker.com`) && Headers(`Upgrade`, `websocket`)'
      - 'traefik.http.routers.my-backend-ws.service=my-backend-ws'
      - 'traefik.http.services.my-backend-ws.loadbalancer.server.port=3000'
      - 'traefik.http.routers.my-backend-ws.tls=true'
      - 'traefik.http.routers.my-backend-ws.tls.certresolver=myresolver'
      - 'traefik.http.middlewares.websocket.headers.customrequestheaders.Upgrade=websocket'
      - 'traefik.http.middlewares.websocket.headers.customrequestheaders.Connection=Upgrade'
      - 'traefik.http.routers.my-backend-ws.middlewares=websocket'

    environment:
      - PGSQL_DATABASE_USER=${PGSQL_DATABASE_USER}
      - PGSQL_DATABASE_PASSWORD=${PGSQL_DATABASE_PASSWORD}
      - PGSQL_DATABASE_NAME=${PGSQL_DATABASE_NAME}
      - PGSQL_DATABASE_PORT=${PGSQL_DATABASE_PORT}
      - PGSQL_DATABASE_HOST=${PGSQL_DATABASE_HOST}

  traefik:
    image: traefik:v2.5
    container_name: traefik
    restart: unless-stopped
    command:
      - '--log.level=DEBUG'
      - '--api.insecure=true'
      - '--providers.docker=true'
      - '--entrypoints.web.address=:80'
      - '--entrypoints.websecure.address=:443'
      - '--certificatesresolvers.myresolver.acme.httpchallenge=true'
      - '--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web'
      - '--certificatesresolvers.myresolver.acme.email=contact@monsupertestdocker.com'
      - '--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json'
    ports:
      - '80:80'
      - '443:443'
      - '8080:8080'
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock'
      - './letsencrypt:/letsencrypt'
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.api.rule=Host(`traefik.monsupertestdocker.com`)'
      - 'traefik.http.services.api.loadbalancer.server.port=8080'
      - 'traefik.http.routers.api.entrypoints=traefik'
      - 'traefik.http.routers.api.service=api@internal'
      - 'traefik.http.routers.api.middlewares=auth'
      - 'traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_USER}:${TRAEFIK_PASSWORD_HASH}'
      - 'traefik.http.routers.api.tls=true'
      - 'traefik.http.routers.api.tls.certresolver=myresolver'
    networks:
      - my-network
    depends_on:
      - my-backend

Result:

  • With Postman at 'http://localhost:3000' everything works. I can authenticate via apiRest and in 'ws://localhost:3000' connect via websocket.

  • In prod via 'https://monsupertestdocker.com/api/login' I can authenticate. But in 'wss://monsupertestdocker.com' I get a 502 error when I want to connect to the websocket.

Here is error by Postman when i try to connect with wss :

Error: Unexpected server response: 502

Handshake Details

Request Method: GET

Status Code: 502 Bad Gateway

Request Headers

Sec-WebSocket-Version: 13

Sec-WebSocket-Key: TqTac3kITFk5Llf0+efqAQ==

Connection: Upgrade

Upgrade: websocket

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Host: monsupertestdocker.com

Response Headers

Date: Wed, 07 Aug 2024 17:26:17 GMT

Content-Length: 11

Content-Type: text/plain; charset=utf-8

here is traefik logs :

time="2024-08-07T15:55:36Z" level=debug msg="vulcand/oxy/roundrobin/rr: begin ServeHttp on request" Request="{\"Method\":\"GET\",\"URL\":{\"Scheme\":\"\",\"Opaque\":\"\",\"User\":null,\"Host\":\"\",\"Path\":\"/\",\"RawPath\":\"\",\"ForceQuery\":false,\"RawQuery\":\"\",\"Fragment\":\"\",\"RawFragment\":\"\"},\"Proto\":\"HTTP/1.1\",\"ProtoMajor\":1,\"ProtoMinor\":1,\"Header\":{\"Connection\":[\"Upgrade\"],\"Sec-Websocket-Extensions\":[\"permessage-deflate; client_max_window_bits\"],\"Sec-Websocket-Key\":[\"4d9l4L3nytw52lblsQzWsQ==\"],\"Sec-Websocket-Version\":[\"13\"],\"Upgrade\":[\"websocket\"],\"X-Forwarded-Host\":[\"monsupertestdocker.com\"],\"X-Forwarded-Port\":[\"443\"],\"X-Forwarded-Proto\":[\"wss\"],\"X-Forwarded-Server\":[\"fd008220b30c\"],\"X-Real-Ip\":[\"93.176.11.97\"]},\"ContentLength\":0,\"TransferEncoding\":null,\"Host\":\"monsupertestdocker.com\",\"Form\":null,\"PostForm\":null,\"MultipartForm\":null,\"Trailer\":null,\"RemoteAddr\":\"93.176.11.97:55834\",\"RequestURI\":\"/\",\"TLS\":null}"

time="2024-08-07T15:55:36Z" level=debug msg="vulcand/oxy/roundrobin/rr: Forwarding this request to URL" ForwardURL="http://172.18.0.4:4200" Request="{\"Method\":\"GET\",\"URL\":{\"Scheme\":\"\",\"Opaque\":\"\",\"User\":null,\"Host\":\"\",\"Path\":\"/\",\"RawPath\":\"\",\"ForceQuery\":false,\"RawQuery\":\"\",\"Fragment\":\"\",\"RawFragment\":\"\"},\"Proto\":\"HTTP/1.1\",\"ProtoMajor\":1,\"ProtoMinor\":1,\"Header\":{\"Connection\":[\"Upgrade\"],\"Sec-Websocket-Extensions\":[\"permessage-deflate; client_max_window_bits\"],\"Sec-Websocket-Key\":[\"4d9l4L3nytw52lblsQzWsQ==\"],\"Sec-Websocket-Version\":[\"13\"],\"Upgrade\":[\"websocket\"],\"X-Forwarded-Host\":[\"monsupertestdocker.com\"],\"X-Forwarded-Port\":[\"443\"],\"X-Forwarded-Proto\":[\"wss\"],\"X-Forwarded-Server\":[\"fd008220b30c\"],\"X-Real-Ip\":[\"93.176.11.97\"]},\"ContentLength\":0,\"TransferEncoding\":null,\"Host\":\"monsupertestdocker.com\",\"Form\":null,\"PostForm\":null,\"MultipartForm\":null,\"Trailer\":null,\"RemoteAddr\":\"93.176.11.97:55834\",\"RequestURI\":\"/\",\"TLS\":null}"

time="2024-08-07T15:55:36Z" level=debug msg="'502 Bad Gateway' caused by: EOF"

time="2024-08-07T15:55:36Z" level=debug msg="vulcand/oxy/roundrobin/rr: completed ServeHttp on request" Request="{\"Method\":\"GET\",\"URL\":{\"Scheme\":\"\",\"Opaque\":\"\",\"User\":null,\"Host\":\"\",\"Path\":\"/\",\"RawPath\":\"\",\"ForceQuery\":false,\"RawQuery\":\"\",\"Fragment\":\"\",\"RawFragment\":\"\"},\"Proto\":\"HTTP/1.1\",\"ProtoMajor\":1,\"ProtoMinor\":1,\"Header\":{\"Connection\":[\"Upgrade\"],\"Sec-Websocket-Extensions\":[\"permessage-deflate; client_max_window_bits\"],\"Sec-Websocket-Key\":[\"4d9l4L3nytw52lblsQzWsQ==\"],\"Sec-Websocket-Version\":[\"13\"],\"Upgrade\":[\"websocket\"],\"X-Forwarded-Host\":[\"monsupertestdocker.com\"],\"X-Forwarded-Port\":[\"443\"],\"X-Forwarded-Proto\":[\"wss\"],\"X-Forwarded-Server\":[\"fd008220b30c\"],\"X-Real-Ip\":[\"93.176.11.97\"]},\"ContentLength\":0,\"TransferEncoding\":null,\"Host\":\"monsupertestdocker.com\",\"Form\":null,\"PostForm\":null,\"MultipartForm\":null,\"Trailer\":null,\"RemoteAddr\":\"93.176.11.97:55834\",\"RequestURI\":\"/\",\"TLS\":null}"

Usually WSS just works over https.

Use 3 backticks before and after code/config to make it more readable and preserve spacing, in yaml every space matters

Oh, I didn't see that. That's it, I've indented the code

Enable and check Traefik access log in JSON format, check for OriginStatus (from target service).