How to prioritize route with websockets?

1. The problem I’m having:

I’m trying to prioritize routing to a WS route before then routing to a catch all API route. With my current config, it sometimes does connect to the WS, and sometimes tries to connect to the catch-all server seemingly randomly.

2. Error messages and/or full log output:

caddy-1  | {"level":"error","ts":1714325787.625634,"logger":"http.log.error","msg":"dial tcp 64.70.19.33:80: i/o timeout","request":{"remote_ip":"MY.IP.ADDRESS","remote_port":"40394","client_ip":"MY.IP.ADDRESS","proto":"HTTP/1.1","method":"GET","host":"botrisbattle.com","uri":"/ws?token=ebb90df6-a63f-41f3-8fe5-ece02bebe8c7&roomKey=ssheavwg5qf1swre1w0kos4c","headers":{"Origin":["https://botrisbattle.com"],"Sec-Websocket-Key":["IEyrb3q3fvoXaVHEkLEOhQ=="],"Sec-Websocket-Version":["13"],"Connection":["Upgrade"],"Upgrade":["websocket"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"","server_name":"botrisbattle.com"}},"duration":3.000491934,"status":502,"err_id":"ca1uysd81","err_trace":"reverseproxy.statusError (reverseproxy.go:1267)"}
caddy-1  | {"level":"error","ts":1714325870.6034312,"logger":"http.log.error","msg":"dial tcp 64.70.19.33:80: i/o timeout","request":{"remote_ip":"MY.IP.ADDRESS","remote_port":"47460","client_ip":"MY.IP.ADDRESS","proto":"HTTP/1.1","method":"GET","host":"botrisbattle.com","uri":"/ws?roomId=mj089eig&spectate=true","headers":{"Sec-Websocket-Version":["13"],"Sec-Websocket-Extensions":["permessage-deflate"],"Sec-Websocket-Key":["ge5QDwqSVs76rb+yFnmjIA=="],"Connection":["keep-alive, Upgrade"],"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"],"Cookie":[],"Sec-Fetch-Dest":["empty"],"Sec-Fetch-Mode":["websocket"],"Upgrade":["websocket"],"Accept-Language":["en-US,en;q=0.5"],"Origin":["https://botrisbattle.com"],"Pragma":["no-cache"],"Accept":["*/*"],"Accept-Encoding":["gzip, deflate, br"],"Sec-Fetch-Site":["same-origin"],"Cache-Control":["no-cache"]},"tls":{"resumed":true,"version":772,"cipher_suite":4865,"proto":"http/1.1","server_name":"botrisbattle.com"}},"duration":3.001959266,"status":502,"err_id":"f914xhw4g","err_trace":"reverseproxy.statusError (reverseproxy.go:1267)"}
caddy-1  | {"level":"error","ts":1714325876.7496252,"logger":"http.log.error","msg":"dial tcp 64.70.19.33:80: i/o timeout","request":{"remote_ip":"MY.IP.ADDRESS","remote_port":"36690","client_ip":"MY.IP.ADDRESS","proto":"HTTP/1.1","method":"GET","host":"botrisbattle.com","uri":"/ws?roomId=mj089eig&spectate=true","headers":{"User-Agent":["Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0"],"Accept":["*/*"],"Origin":["https://botrisbattle.com"],"Cache-Control":["no-cache"],"Accept-Language":["en-US,en;q=0.5"],"Sec-Websocket-Key":["frKS0AFJmv0uVBSiNojkIA=="],"Connection":["keep-alive, Upgrade"],"Accept-Encoding":["gzip, deflate, br"],"Sec-Fetch-Dest":["empty"],"Sec-Fetch-Site":["same-origin"],"Pragma":["no-cache"],"Upgrade":["websocket"],"Sec-Websocket-Version":["13"],"Sec-Websocket-Extensions":["permessage-deflate"],"Cookie":[],"Sec-Fetch-Mode":["websocket"]},"tls":{"resumed":true,"version":772,"cipher_suite":4865,"proto":"http/1.1","server_name":"botrisbattle.com"}},"duration":3.000643678,"status":502,"err_id":"pg2qkww33","err_trace":"reverseproxy.statusError (reverseproxy.go:1267)"}

3. Caddy version:

v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=

4. How I installed and ran Caddy:

Using a docker image

a. System environment:

Docker, Debian 12

b. Command:

I run it through a docker container

services:
  caddy:
    image: caddy:2.7.6
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - frontend

c. Service/unit/compose file:

services:
  caddy:
    image: caddy:2.7.6
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./site:/srv
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - frontend
  server:
    container_name: server
    restart: unless-stopped
    env_file:
      - .prod.env
    environment:
      NUXT_BUILD: "false"
      NODE_ENV: "production"
      NUXT_ENVIRONMENT: "production"
    build:
      context: .
    ports:
      - 3000:3000
      - 8080:8080
    depends_on:
      db:
        condition: service_healthy
    networks:
      - frontend
  db:
    container_name: db
    image: mysql
    environment:
        MYSQL_DATABASE: "botris"
        MYSQL_ROOT_PASSWORD: "password"
        # MYSQL_USER: "root"
        # MYSQL_PASSWORD: "password"
        # MYSQL_DATABASE: "database"
    restart: unless-stopped
    volumes:
      - db:/var/lib/botris-db
    healthcheck:
        test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
        timeout: 20s
        retries: 10
    networks:
      - frontend
volumes:
  db:
  caddy_data:
  caddy_config:
networks:
  frontend:

d. My complete Caddy config:

botrisbattle.com {
	@websockets {
		header Connection *Upgrade*
		header Upgrade    websocket
	}

	handle /ws {
		reverse_proxy @websockets /ws server:8080
	}

	handle {
    	reverse_proxy server:3000
	}
}

5. Links to relevant resources:

Does your websocket always use /ws as the path? If so all you need is this:

botrisbattle.com {
	reverse_proxy /ws server:8080
	reverse_proxy server:3000
}

Otherwise if it could use any path, you should do this:

botrisbattle.com {
	@websockets {
		header Connection *Upgrade*
		header Upgrade    websocket
	}
	reverse_proxy @websockets server:8080
	reverse_proxy server:3000
}

The first solution appears to be working, thanks!