Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions apps/relay-docker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Clawket Self-Hosted Relay — Environment Configuration
# Copy this file to .env and adjust the values for your deployment.

# ---------- Ports ----------
# Port for the Registry HTTP API
REGISTRY_PORT=3001
# Port for the Relay WebSocket server
RELAY_PORT=3002

# ---------- URLs ----------
# The public WebSocket URL clients will use to connect to the relay.
# Must be wss:// in production (behind nginx TLS termination).
RELAY_URL=wss://relay.example.com/ws

# The public URL of the registry API.
# Used by the relay to verify tokens against the registry.
REGISTRY_URL=https://registry.example.com

# ---------- Region Map (optional) ----------
# Optional JSON map of region → public relay WebSocket URL for this same deployment.
# relay-docker is a single-process / single-instance deployment: do not point these URLs
# at different relay nodes unless you also add your own cross-node state coordination.
# Leave empty to use RELAY_URL for every region.
# Example: {"cn":"wss://relay-cn.example.com/ws","sg":"wss://relay-sg.example.com/ws"}
RELAY_REGION_MAP=

# ---------- Persistence ----------
# Path to SQLite database file for KV persistence.
# Leave empty for memory-only mode (data lost on restart).
# For Docker, mount a volume and set: /data/kv.sqlite
KV_PERSIST_PATH=/data/kv.sqlite

# ---------- Pairing ----------
# Access code TTL in seconds (default: 600 = 10 minutes)
PAIR_ACCESS_CODE_TTL_SEC=600

# Maximum client tokens per gateway (default: 8)
PAIR_CLIENT_TOKEN_MAX=8

# ---------- Rate Limiting ----------
MAX_MESSAGES_PER_10S=120
MAX_CLIENT_MESSAGES_PER_10S=300

# ---------- Timeouts ----------
HEARTBEAT_INTERVAL_MS=30000
AWAITING_CHALLENGE_TTL_MS=25000
CLIENT_IDLE_TIMEOUT_MS=600000
GATEWAY_OWNER_LEASE_MS=20000
57 changes: 57 additions & 0 deletions apps/relay-docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ---------- Build Stage ----------
FROM node:22-alpine AS builder

ARG NPM_REGISTRY=https://registry.npmjs.org/

WORKDIR /build

# Copy root workspace files
COPY package.json package-lock.json ./
COPY tsconfig.relay-base.json ./

# Copy shared package (source — used as workspace dependency)
COPY packages/relay-shared/ ./packages/relay-shared/

# Copy relay-docker app
COPY apps/relay-docker/ ./apps/relay-docker/

# Install ALL dependencies (including devDependencies for build)
# Use npm install so this image can still build when the monorepo lockfile changes outside relay-docker.
# --ignore-scripts: skip root postinstall (mobile-only setup not needed here)
RUN npm install --workspace=@clawket/relay-docker --workspace=@clawket/shared --include-workspace-root --ignore-scripts --registry=${NPM_REGISTRY}

# Build relay-docker (tsc)
RUN npm run --workspace=@clawket/relay-docker build

# Build the shared package runtime bundle for production consumption.
RUN npm run --workspace=@clawket/shared build

# ---------- Production Stage ----------
FROM node:22-alpine

ARG NPM_REGISTRY=https://registry.npmjs.org/

WORKDIR /app

# Copy compiled relay-docker output
COPY --from=builder /build/apps/relay-docker/dist/ ./dist/
COPY --from=builder /build/apps/relay-docker/package.json ./

# Copy the shared package runtime artifacts prepared by its dedicated build script
COPY --from=builder /build/packages/relay-shared/dist/ ./node_modules/@clawket/shared/

# Install production dependencies only (ws, better-sqlite3)
RUN npm install --omit=dev --ignore-scripts --registry=${NPM_REGISTRY} \
&& npm rebuild better-sqlite3

# Create data directory for SQLite persistence
RUN mkdir -p /data

EXPOSE 3001 3002

ENV NODE_ENV=production
ENV KV_PERSIST_PATH=/data/kv.sqlite

VOLUME /data

CMD ["node", "dist/server.js"]
116 changes: 116 additions & 0 deletions apps/relay-docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Clawket Self-Hosted Relay (Docker)

`relay-docker` is the self-hosted Docker deployment of Clawket's relay stack. It runs the registry HTTP API and the relay WebSocket server inside one Node.js process, with one shared in-process KV layer and optional SQLite persistence.

## What This Deployment Supports

- One logical deployment unit: one registry + one relay process.
- Optional multiple public relay URLs via `RELAY_REGION_MAP`, but every URL must still route to this same deployment.
- Persistent pairing records when `KV_PERSIST_PATH` points to a writable SQLite file.

It does not provide cross-node coordination, distributed room ownership, or multi-instance token propagation.

## Quick Start

### 1. Configure Environment

```bash
cp .env.example .env
# Edit .env with your domains and persistence path
```

### 2. Build and Run

```bash
docker compose up -d --build

docker compose logs -f

curl http://localhost:3001/v1/health
curl http://localhost:3002/v1/health
```

The checked-in `docker-compose.yml` builds from the local repository by default so the container matches the code in this workspace.

### 3. Configure nginx

Copy `nginx.conf.example` and replace `registry.example.com` / `relay.example.com` with your real domains. The relay domain only exposes:

- `GET /v1/health`
- `WS /ws`

## Architecture

```
┌─────────────────────────────────────────────┐
│ Single Node.js Process │
│ │
│ Registry HTTP API (:3001) │
│ ├── POST /v1/pair/register │
│ ├── POST /v1/pair/access-code │
│ ├── POST /v1/pair/claim │
│ ├── GET /v1/verify/:gatewayId │
│ └── GET /v1/health │
│ │
│ Relay WebSocket Server (:3002) │
│ ├── WS /ws │
│ └── GET /v1/health │
│ │
│ Shared MemoryKV (+ optional SQLite) │
│ Room Manager (one room per gatewayId) │
└─────────────────────────────────────────────┘
```

## Security Notes

- The relay no longer exposes an HTTP token-sync endpoint. Registry and relay share the same in-process KV state directly.
- Registry JSON request bodies are hard-limited because the API only accepts small pairing payloads.
- If `KV_PERSIST_PATH` is set but SQLite cannot open or write the database, startup or writes fail immediately instead of silently falling back to memory-only behavior.

## Environment Variables

See [`.env.example`](.env.example) for the full list.

Key variables:

| Variable | Default | Description |
|---|---|---|
| `REGISTRY_PORT` | `3001` | Registry API port |
| `RELAY_PORT` | `3002` | Relay WebSocket port |
| `RELAY_URL` | `ws://localhost:3002/ws` | Public relay WebSocket URL |
| `RELAY_REGION_MAP` | _(empty)_ | Optional region → public WebSocket URL map for this same deployment |
| `KV_PERSIST_PATH` | _(empty)_ | Writable SQLite path for persistent pairing records |

## Differences from Cloudflare Deployment

| Feature | Cloudflare | Docker |
|---|---|---|
| KV Storage | Cloudflare KV | In-process memory + optional SQLite |
| Room State | Durable Objects | In-memory per process |
| WebSocket | Hibernatable WS | `ws` library |
| Scaling | Edge / multi-region platform primitives | Single deployment unit |
| Region Detection | `request.cf.country` | `X-Real-Country` header |
| Heartbeat | DO alarms | `setTimeout` alarms |

## Persistence

With `KV_PERSIST_PATH` set:

- Pairing records survive process restarts.
- Active WebSocket connections are lost on restart and clients must reconnect.
- Startup fails if the SQLite file cannot be opened.

Without `KV_PERSIST_PATH`:

- The service runs in memory-only mode.
- All pairing state is lost on restart.

## Development

```bash
npm install

npm run --workspace=@clawket/relay-docker dev
npm run --workspace=@clawket/relay-docker typecheck
npm run --workspace=@clawket/relay-docker build
```
116 changes: 116 additions & 0 deletions apps/relay-docker/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Clawket 自托管 Relay(Docker)

`relay-docker` 是 Clawket relay 栈的 Docker 自托管部署方式。它把 registry HTTP API 和 relay WebSocket 服务放进同一个 Node.js 进程里运行,共享一套进程内 KV,并可选用 SQLite 做持久化。

## 这套部署实际支持什么

- 一个逻辑部署单元:一个 registry + 一个 relay 进程。
- 可以通过 `RELAY_REGION_MAP` 暴露多个公网 relay URL,但这些 URL 最终都必须回到同一套部署。
- 当 `KV_PERSIST_PATH` 指向可写的 SQLite 文件时,配对记录可持久化。

它不提供跨节点协调、分布式房间所有权,也不提供多实例之间的 token 传播。

## 快速开始

### 1. 配置环境变量

```bash
cp .env.example .env
# 按你的域名和持久化路径修改 .env
```

### 2. 构建并运行

```bash
docker compose up -d --build

docker compose logs -f

curl http://localhost:3001/v1/health
curl http://localhost:3002/v1/health
```

仓库里的 `docker-compose.yml` 默认会直接从当前代码构建镜像,保证容器运行的就是你正在审阅和修改的这份代码。

### 3. 配置 nginx

复制 `nginx.conf.example`,把 `registry.example.com` 和 `relay.example.com` 替换成你的真实域名。relay 域名只暴露:

- `GET /v1/health`
- `WS /ws`

## 架构

```
┌─────────────────────────────────────────────┐
│ Single Node.js Process │
│ │
│ Registry HTTP API (:3001) │
│ ├── POST /v1/pair/register │
│ ├── POST /v1/pair/access-code │
│ ├── POST /v1/pair/claim │
│ ├── GET /v1/verify/:gatewayId │
│ └── GET /v1/health │
│ │
│ Relay WebSocket Server (:3002) │
│ ├── WS /ws │
│ └── GET /v1/health │
│ │
│ Shared MemoryKV (+ optional SQLite) │
│ Room Manager (one room per gatewayId) │
└─────────────────────────────────────────────┘
```

## 安全说明

- relay 不再暴露 HTTP token 同步接口。registry 和 relay 直接共享同一份进程内 KV 状态。
- registry 的 JSON 请求体做了硬限制,因为它只接受很小的配对请求。
- 如果设置了 `KV_PERSIST_PATH`,但 SQLite 不能打开或写入,服务会立即启动失败或请求失败,而不是悄悄退化成纯内存模式。

## 环境变量

完整说明见 [`.env.example`](.env.example)。

关键变量:

| 变量 | 默认值 | 说明 |
|---|---|---|
| `REGISTRY_PORT` | `3001` | Registry API 端口 |
| `RELAY_PORT` | `3002` | Relay WebSocket 端口 |
| `RELAY_URL` | `ws://localhost:3002/ws` | 对外提供的 relay WebSocket URL |
| `RELAY_REGION_MAP` | _(空)_ | 当前这套部署的可选 region → 公网 WebSocket URL 映射 |
| `KV_PERSIST_PATH` | _(空)_ | 持久化配对记录的可写 SQLite 路径 |

## 与 Cloudflare 部署的差异

| 功能 | Cloudflare | Docker |
|---|---|---|
| KV 存储 | Cloudflare KV | 进程内内存 + 可选 SQLite |
| 房间状态 | Durable Objects | 单进程内存 |
| WebSocket | Hibernatable WS | `ws` 库 |
| 扩展方式 | 边缘平台 / 多区域原语 | 单部署单元 |
| Region 判定 | `request.cf.country` | `X-Real-Country` 请求头 |
| 心跳 | DO alarms | `setTimeout` alarm |

## 持久化

设置 `KV_PERSIST_PATH` 时:

- 配对记录可以跨进程重启保留。
- 活跃 WebSocket 连接会在重启时断开,客户端需要重连。
- 如果 SQLite 文件无法打开,启动会直接失败。

不设置 `KV_PERSIST_PATH` 时:

- 服务以纯内存模式运行。
- 进程重启后所有配对状态都会丢失。

## 开发

```bash
npm install

npm run --workspace=@clawket/relay-docker dev
npm run --workspace=@clawket/relay-docker typecheck
npm run --workspace=@clawket/relay-docker build
```
31 changes: 31 additions & 0 deletions apps/relay-docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
services:
clawket-relay:
build:
context: ../../
dockerfile: apps/relay-docker/Dockerfile
# 如需使用已发布镜像,可改为:
# image: weirdoyh/clawket-relay:latest
container_name: clawket-relay
restart: unless-stopped
ports:
- "127.0.0.1:3001:3001"
- "127.0.0.1:3002:3002"
volumes:
- clawket-data:/data
env_file:
- .env
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/v1/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"

volumes:
clawket-data:
driver: local
Loading
Loading