Solving cloudflared's Random URLs — How I Fixed LINE Bot Permanently with a WebSocket Relay Server
The Problem: URL Changes Every Restart
I wanted to run a LINE Bot on a home network PC (a GMK mini PC).
LINE Webhooks require a fixed HTTPS URL. To expose a PC with only a private IP to the outside world, you need some kind of tunnel. My first choice was cloudflared (Cloudflare Tunnel).
cloudflared tunnel --url http://localhost:5000
This generates a temporary HTTPS URL that can be used for the webhook. It seemed perfect at first.
The issue was the "temporary" part.
Every time the PC restarts, the URL changes. Something like https://random-words-1234.trycloudflare.com. You have to log into the LINE Developers Console and manually update the Webhook URL every single time.
For a personal bot that's barely tolerable, but I was trying to embed this into a consumer-facing product (the GMK AI mini PC). You can't tell users "please update the URL after every restart."
Options I Considered
| Approach | Problem |
|---|---|
| cloudflared named tunnel | Requires Cloudflare auth — too complex for end users |
| ngrok fixed URL | Requires paid plan |
| Port forwarding | Depends on user's router config — not practical |
| WebSocket relay server | ← This is what I went with |
The Solution: Build Your Own Relay Server
A shift in thinking. Instead of "expose the PC as a server to the outside," I changed to "have the PC connect out to a server."
LINE Platform
↓ Webhook (HTTPS)
https://linebot.techsfree.com/gmk/{token}/callback ← Fixed URL!
↓ Nginx (443) → FastAPI (3500)
web server: gmk_relay.py ← Relay server
↓ WebSocket forwarding
GMK PC: line_bridge_v2.py ← WebSocket client
↓ OpenClaw chatCompletions API
↓ LINE Reply API (direct)
The key is that the direction is reversed.
Old way: LINE → cloudflared → GMK PC (outside comes into the PC)
New way: LINE → relay server ← GMK PC (PC maintains connection to server)
If you keep a persistent WebSocket connection from the PC to the server, NAT traversal and firewalls become irrelevant. WebSocket is client-initiated, so it works in any environment.
Implementation
Relay server side (FastAPI + WebSocket)
# Core of gmk_relay.py (conceptual)
app = FastAPI()
# Hold WebSocket connections for each GMK unit
connections: dict[str, WebSocket] = {}
@app.websocket("/gmk/{token}/ws")
async def websocket_endpoint(websocket: WebSocket, token: str):
await websocket.accept()
connections[token] = websocket
try:
while True:
await websocket.receive_text() # keep alive ping/pong
except:
del connections[token]
@app.post("/gmk/{token}/callback")
async def line_webhook(token: str, request: Request):
body = await request.body()
ws = connections.get(token)
if ws:
await ws.send_text(body.decode()) # Forward to GMK
return {"status": "ok"}
GMK PC side (WebSocket client)
# Core of line_bridge_v2.py (conceptual)
async def run():
relay_url = f"wss://linebot.techsfree.com/gmk/{token}/ws"
async with websockets.connect(relay_url) as ws:
async for message in ws:
data = json.loads(message)
# Process LINE event and generate AI reply
reply = await call_openclaw(data)
await send_line_reply(reply)
Each GMK unit has a unique relay_token at startup and maintains a connection to the relay server. The LINE Webhook URL is fixed at https://linebot.techsfree.com/gmk/{token}/callback. It never changes after a restart.
The Gotcha: Hairpin NAT
I hit one snag during testing.
When the test GMK (on the local network) tried to connect to wss://linebot.techsfree.com/..., it timed out. The relay server was also on the same LAN, so hairpin NAT (accessing an external domain from within the same LAN) was being blocked by the router.
Solution: change only the test machine to connect via direct IP.
{
"relay_url": "ws://192.168.x.x:3500/gmk/{token}/ws"
}
Production users' devices are on different networks (their home LAN), so wss://linebot.techsfree.com/... works fine for them.
Results
- ✅ Fixed Webhook URL (doesn't change on restart)
- ✅ No cloudflared dependency (fewer moving parts)
- ✅ Simple user setup: just paste a URL
- ✅ One relay server handles multiple GMK units
The final user setup flow looks like this:
- Boot the GMK
- Create a LINE Bot in LINE Developers (get Channel Secret/Token)
- Enter Secret/Token in the Dashboard UI
- Paste the displayed fixed URL into LINE Console
- Done
That's consumer-product quality.
Summary
By shifting from "expose a service externally" to "have the service connect out to a server," we solved the cloudflared random URL problem at its root.
This pattern of using persistent WebSocket connections has applications beyond LINE Bots — IoT device remote management, reverse proxies for home servers, and more.
Tags: linebot, websocket, fastapi, nat, python, infrastructure, openclaw
Top comments (0)