V2Ray over WebSocket and TLS with Caddy

This setup runs a V2Ray server that accepts VMess over WebSocket on a local port, and puts Caddy in front of it. Caddy obtains a TLS certificate for your domain automatically and forwards only WebSocket upgrade requests to V2Ray, so the traffic looks like ordinary HTTPS on port 443. Both services run with Docker Compose.

You need a domain (proxy.example.com below) whose DNS record points at the server, and ports 80 and 443 open so Caddy can complete the ACME challenge.

V2Ray

Configuration

Generate a client ID:

uuidgen

Put it in config.json:

{
  "log": {
    "loglevel": "warning"
  },
  "inbound": {
    "port": 10086,
    "listen": "0.0.0.0",
    "protocol": "vmess",
    "settings": {
      "clients": [
        {
          "id": "<your-uuid>",
          "alterId": 64
        }
      ]
    },
    "streamSettings": {
      "network": "ws"
    }
  },
  "outbound": {
    "protocol": "freedom",
    "settings": {}
  }
}

Set loglevel to debug while you troubleshoot. Clients must use the same alterId. Newer V2Ray clients use VMess AEAD with alterId 0. In that case, set it to 0 here as well.

Docker Compose

services:
  v2ray:
    image: v2ray/official
    volumes:
      - ./config.json:/etc/v2ray/config.json
    ports:
      - 127.0.0.1:10086:10086
    restart: unless-stopped

The port is bound to 127.0.0.1, so the unencrypted WebSocket endpoint is reachable only through Caddy.

Caddy

Caddyfile

{
  # debug
}
proxy.example.com {
  # Serve a static site for normal visitors (optional)
  # root * /var/www/website
  # file_server

  @v2ray_websocket {
    path /
    header Connection *Upgrade*
    header Connection *upgrade*
    header Upgrade websocket
  }
  reverse_proxy @v2ray_websocket 127.0.0.1:10086
}

The @v2ray_websocket matcher selects WebSocket upgrade requests on /. Caddy treats the two Connection values as alternatives, so either capitalization matches. Uncomment root and file_server to serve a decoy website from ./fake_root to everyone else.

Docker Compose

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./fake_root:/var/www/website
      - ./data:/data
      - ./config:/config

network_mode: host lets Caddy reach V2Ray on 127.0.0.1:10086 and listen on ports 80 and 443 directly. Keep ./data, because it stores the certificates.

Client settings

Field Value
Protocol VMess
Address proxy.example.com
Port 443
UUID <your-uuid>
alterId same as the server
Network ws
Path /
TLS enabled
SNI / Host proxy.example.com



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Debugging Nextcloud LDAP with occ
  • Complete Guide to Intel i3-12100 iGPU SR-IOV on Proxmox VE 9
  • NFS over RDMA Configuration Guide
  • InfiniBand Quick Command Reference
  • Exposing an Internal SSH Port with FRP and Docker Compose