AnyCable-Go 1.5 Docker Compose 部署指南
用 Docker Compose 一键编排 anycable-go 1.5.6 + Redis 7 + Ruby RPC 完整实时推送栈,完成 WS 订阅、HTTP 广播、Redis 广播三链路端到端实测,附官方 Helm Chart 的 Kubernetes 部署形态。
AnyCable-Go 1.5 Docker Compose 部署指南
目标:用 Docker Compose 一键编排 AnyCable 完整实时推送栈——anycable-go 1.5.6(WebSocket 网关)+ Redis 7(广播通道)+ Ruby RPC 后端(鉴权与频道逻辑),并完成 WebSocket 订阅、HTTP 广播、Redis 广播三条链路的端到端联通实测。篇末附节给出官方 Helm Chart 的 Kubernetes 部署形态。本文所有命令与回显均已实测通过(查询/实验日期 2026-10-25)。
一、环境信息
- Docker Engine:29.9.2
- Docker Compose:v5.6.1(compose plugin)
- anycable-go 镜像:
anycable/anycable-go:1.5.6(48.6MB,本文实测基线;2026-10-25 实查单仓 anycable/anycable 最新稳定版为 1.6.20,选型说明见 A1 篇) - Redis 镜像:
redis:7-alpine(57.8MB) - RPC 后端镜像:基于
ruby:3.0-slim本地构建(anycable gem 1.5.2 + litecable 0.8.2) - 宿主机端口分配(避开本机已占用端口后选取):
| 宿主机端口 | 容器端口 | 用途 |
|---|---|---|
| 8139 | 8080 | WebSocket /cable 与健康检查 /health |
| 8140 | 8090 | HTTP 广播端点 /_broadcast |
| 8141 | 9091 | Prometheus 指标 /metrics |
💡 技术拓展:为什么 AnyCable 需要三个容器而不是一个——
角色 职责 不部署会怎样 anycable-go 扛海量 WebSocket 长连接、收发消息 整个服务不存在 RPC 后端 连接鉴权、频道订阅授权、业务回调 客户端能连上但订阅任何频道都会被拒( grpc connection is not ready)Redis 业务应用到 anycable-go、以及多节点之间的广播总线 应用无法把消息推给客户端 如果你不需要鉴权、只收公开流,也可以省略 RPC 容器,改用
--norpc+ JWT 的独立模式(参见本专栏《AnyCable 1.5 配置参数详解》)。
二、整体架构
浏览器/客户端 ──WS──▶ anycable-go:8080 ◀──gRPC(50051)── rpc(Ruby 业务应用)
▲ ▲ │
HTTP广播 :8090│ │Redis订阅 __anycable__ │AnyCable.broadcast()
│ └──────── redis:6379 ◀───────┘
业务系统 POST
- 客户端只和 anycable-go 说话;鉴权决策通过 gRPC 实时询问 RPC 后端。
- 业务应用广播消息有两条路:Ruby 侧
AnyCable.broadcast()写入 Redis 的__anycable__频道(推荐,多节点通用);或直接向 anycable-go 的 HTTP 广播端点 POST(单节点最简单)。 - 三个容器处于同一个 compose 内部网络,按服务名互访(
rpc:50051、redis:6379)。
三、前置条件
docker version --format 'Server: {{.Server.Version}}'
docker compose version
实测回显:
Server: 29.9.2
Docker Compose version v5.6.1
⚠️ 网络说明:本文实验机可直连 Docker Hub 拉取镜像。若你的环境拉取超时,请先为 Docker daemon 配置镜像加速或代理,再继续。
四、编排文件
4.1 目录结构
mkdir -p /opt/anycable-stack
cd /opt/anycable-stack
最终目录包含三个文件:
/opt/anycable-stack/
├── docker-compose.yml # 编排定义
├── rpc.Dockerfile # RPC 后端镜像构建
└── chat_app.rb # 最小聊天频道应用(挂载进 RPC 镜像)
4.2 docker-compose.yml
name: anycable-1.5.6-stack
services:
redis:
image: redis:7-alpine
container_name: anycable-1.5.6-redis
command: ["redis-server", "--appendonly", "no"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
rpc:
build:
context: .
dockerfile: rpc.Dockerfile
image: anycable-1.5.6-rpc:local
container_name: anycable-1.5.6-rpc
environment:
ANYCABLE_REDIS_URL: redis://redis:6379/0
ANYCABLE_RPC_HOST: 0.0.0.0:50051
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
anycable-go:
image: anycable/anycable-go:1.5.6
container_name: anycable-1.5.6-go
environment:
ANYCABLE_HOST: 0.0.0.0
ANYCABLE_PORT: 8080
ANYCABLE_RPC_HOST: rpc:50051
ANYCABLE_REDIS_URL: redis://redis:6379/0
ANYCABLE_HTTP_BROADCAST_PORT: 8090
ANYCABLE_BROADCAST_KEY: q1w2e3r4
ANYCABLE_LOG_LEVEL: info
ANYCABLE_METRICS_HTTP: /metrics
ANYCABLE_METRICS_PORT: 9091
ports:
- "8139:8080"
- "8140:8090"
- "8141:9091"
depends_on:
redis:
condition: service_healthy
rpc:
condition: service_started
restart: unless-stopped
关键设计逐项说明:
| 配置项 | 作用 | 取值理由 |
|---|---|---|
name: anycable-1.5.6-stack | 显式命名 compose 项目 | 避免默认取目录名导致多版本并存时互相覆盖 |
ANYCABLE_RPC_HOST: rpc:50051 | 告诉 anycable-go 去哪做鉴权 | 指向内部 RPC 服务名,非环回地址 |
ANYCABLE_HTTP_BROADCAST_PORT: 8090 | 开启 HTTP 广播端点 | 默认 0=关闭,必须显式开 |
ANYCABLE_BROADCAST_KEY | HTTP 广播鉴权密钥 | 不设则广播端点裸奔;演示值 q1w2e3r4,生产请替换 |
redis healthcheck + depends_on.condition | 保证依赖就绪再启动 | 避免 anycable-go 启动时订阅 Redis 失败反复重启 |
restart: unless-stopped | 崩溃自愈 | 长连接网关必须自动拉起 |
⚠️ 注意:
container_name与docker compose up --scale互斥(固定容器名无法复制多份)。如需横向扩容 anycable-go,去掉其container_name再--scale anycable-go=2,前置负载均衡即可——anycable-go 无状态,天然支持多副本。
4.3 RPC 后端镜像(rpc.Dockerfile)
anycable-go 只负责扛连接,鉴权与频道逻辑在 Ruby 应用里。生产上这是你的 Rails 应用(挂 anycable gem),本文用一个最小 litecable 应用代替:
FROM ruby:3.0-slim
RUN gem install anycable -v 1.5.2 --no-document \
&& gem install litecable redis rack --no-document
WORKDIR /app
COPY chat_app.rb /app/chat_app.rb
EXPOSE 50051
CMD ["anycable", "-r", "./chat_app.rb"]
⚠️ 注意:anycable gem 必须钉在
-v 1.5.2,与 anycable-go 1.5.6 同代。直接gem install anycable会装到 1.6.x,跨代协议不兼容。
4.4 最小频道应用(chat_app.rb)
require "anycable"
require "rack"
require "lite_cable"
AnyCable.connection_factory = LiteCable::Connection::Base
class ChatChannel < LiteCable::Channel::Base
def subscribed
stream_from "chat"
transmit({ type: "notice", text: "joined chat" })
end
end
LiteCable.channel_registry.add("ChatChannel", ChatChannel)
require "rack":litecable 适配层内部引用Rack::Request,缺了会报uninitialized constant Rack。subscribed:订阅成功即挂到chat流并回推一条欢迎消息,便于验证链路。channel_registry.add:litecable 按注册表解析频道名,不注册会报Unknown channel: ChatChannel。
五、构建、启动与验证
5.1 构建 RPC 镜像并启动全栈
docker compose build
docker compose up -d
启动完成后查看状态:
docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'
实测回显:
NAME STATUS PORTS
anycable-1.5.6-go Up 4 seconds 0.0.0.0:8139->8080/tcp, [::]:8139->8080/tcp, 0.0.0.0:8140->8090/tcp, [::]:8140->8090/tcp, 0.0.0.0:8141->9091/tcp, [::]:8141->9091/tcp
anycable-1.5.6-redis Up 10 seconds (healthy) 6379/tcp
anycable-1.5.6-rpc Up 4 seconds 50051/tcp
5.2 启动日志确认
docker logs anycable-1.5.6-go
实测回显(节选关键行):
2026-10-25 02:46:07.408 INF Starting AnyCable 1.5.6-e0dd6fa (pid: 1, open file limit: 524288, gomaxprocs: 6) nodeid=VapSZB
2026-10-25 02:46:07.408 INF Using channels router: $pubsub nodeid=VapSZB
2026-10-25 02:46:07.408 INF Accept broadcast requests at http://0.0.0.0:8090/_broadcast (authorization required) nodeid=VapSZB context=broadcast provider=http
2026-10-25 02:46:07.408 INF RPC controller initialized: rpc:50051 (concurrency: 28, impl: grpc, enable_tls: false, proto_versions: v1, proxy_headers: cookie, proxy_cookies: <all>) nodeid=VapSZB context=rpc
2026-10-25 02:46:07.408 INF Handle WebSocket connections at http://0.0.0.0:8080/cable nodeid=VapSZB
2026-10-25 02:46:07.408 INF Handle health requests at http://0.0.0.0:8080/health nodeid=VapSZB
2026-10-25 02:46:07.409 INF Serve metrics at http://0.0.0.0:9091/metrics nodeid=VapSZB context=metrics
2026-10-25 02:46:07.410 INF subscribed to Redis channel nodeid=VapSZB context=broadcast provider=redis channel=__anycable__
四个关键信号:gRPC 控制器已指向 rpc:50051、HTTP 广播端点已开且要求鉴权、Redis 广播频道 __anycable__ 已订阅、metrics 已暴露。RPC 容器日志确认 gRPC 监听:
docker logs anycable-1.5.6-rpc
实测回显(节选):
I, [2026-10-25T02:46:06.843684 #1] INFO -- : gRPC version: 1.73.0
I, [2026-10-25T02:46:06.872857 #1] INFO -- : Broadcasting Redis channel: __anycable__
I, [2026-10-25T02:46:06.874395 #1] INFO -- : RPC server is listening on 0.0.0.0:50051 (workers_num: 30)
5.3 验证 1:健康检查
curl -s http://127.0.0.1:8139/health
实测回显:
Ah, ha, ha, ha, stayin' alive, stayin' alive.
5.4 验证 2:WebSocket 订阅 → HTTP 广播 → 客户端收到
编写验证脚本 /opt/anycable-stack/ws_test.py(依赖 pip3 install websockets):
import asyncio, json, urllib.request
import websockets
WS_URL = "ws://127.0.0.1:8139/cable"
BC_URL = "http://127.0.0.1:8140/_broadcast"
async def recv_until(ws, pred, timeout=8):
async def _loop():
while True:
raw = await ws.recv()
print("FRAME:", raw)
if pred(json.loads(raw)):
return json.loads(raw)
return await asyncio.wait_for(_loop(), timeout=timeout)
def http_broadcast(stream, payload):
body = json.dumps({"stream": stream, "data": json.dumps(payload)}).encode()
req = urllib.request.Request(BC_URL, data=body, method="POST",
headers={"Content-Type": "application/json", "Authorization": "Bearer q1w2e3r4"})
with urllib.request.urlopen(req) as r:
return r.status
async def main():
async with websockets.connect(WS_URL) as ws:
await recv_until(ws, lambda o: o.get("type") == "welcome")
print("==> WELCOME OK")
sub = {"command": "subscribe", "identifier": json.dumps({"channel": "ChatChannel"})}
await ws.send(json.dumps(sub))
await recv_until(ws, lambda o: o.get("type") == "confirm_subscription")
print("==> CONFIRMED ChatChannel")
code = http_broadcast("chat", {"text": "hello from compose"})
print("==> HTTP_BROADCAST_STATUS:", code)
msg = await recv_until(ws, lambda o: o.get("message") and o["message"].get("text") == "hello from compose")
print("==> MSG_OK:", json.dumps(msg, ensure_ascii=False))
asyncio.run(main())
print("==> ALL_OK")
执行:
python3 /opt/anycable-stack/ws_test.py
实测回显:
FRAME: {"type":"welcome"}
==> WELCOME OK
FRAME: {"identifier":"{\"channel\": \"ChatChannel\"}","message":{"type":"notice","text":"joined chat"}}
FRAME: {"identifier":"{\"channel\": \"ChatChannel\"}","type":"confirm_subscription"}
==> CONFIRMED ChatChannel
==> HTTP_BROADCAST_STATUS: 201
FRAME: {"identifier":"{\"channel\": \"ChatChannel\"}","message":{"text":"hello from compose"}}
==> MSG_OK: {"identifier": "{\"channel\": \"ChatChannel\"}", "message": {"text": "hello from compose"}}
==> ALL_OK
这条链路覆盖了全部三个容器:WS 握手 → gRPC 鉴权(rpc 容器批准订阅)→ HTTP 广播(anycable-go 容器接收)→ 客户端收到消息。
⚠️ 注意:HTTP 广播报文中
data字段必须是 JSON 编码后的字符串(脚本里json.dumps(payload)再嵌入),直接传对象形态 anycable-go 解析后不会投递,这是实测踩过的坑。
5.5 验证 3:广播鉴权拦截
不带密钥直接 POST 广播端点:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8140/_broadcast \
-H 'Content-Type: application/json' \
-d '{"stream":"chat","data":"{\"text\":\"x\"}"}'
实测回显:
401
ANYCABLE_BROADCAST_KEY 生效,未授权请求被拒。
5.6 验证 4:Redis 广播通道
模拟 Ruby 应用经 Redis 广播(生产上 AnyCable.broadcast() 走的就是这条路):
docker exec anycable-1.5.6-redis redis-cli PUBLISH __anycable__ '{"stream":"chat","data":"{\"text\":\"via redis pub\"}"}'
实测回显:
1
返回值 1 表示有 1 个订阅者(anycable-go)收到该消息;结合 5.4 的 WS 客户端即可确认消息被投递。
5.7 验证 5:Prometheus 指标
curl -s http://127.0.0.1:8141/metrics | grep -E '^anycable_go_(clients_num|goroutines_num|rpc_call_total|broadcast_msg_total)'
实测回显:
anycable_go_broadcast_msg_total 2
anycable_go_rpc_call_total 3
anycable_go_goroutines_num 87
anycable_go_clients_num 0
rpc_call_total 3 对应刚才的连接鉴权与订阅授权调用,broadcast_msg_total 2 对应两次广播投递,指标与行为完全吻合。
六、日常运维命令
# 查看实时日志(逐行一个服务)
docker compose logs -f anycable-go
docker compose logs -f rpc
# 重启 anycable-go(例如改了环境变量后)
docker compose restart anycable-go
# 修改 compose 文件后重建并滚动更新
docker compose up -d --build
# 停止并删除整个栈(容器与内部网络)
docker compose down
升级 anycable-go 版本时,修改 docker-compose.yml 中的镜像 tag(例如 anycable/anycable-go:1.6.0),然后 docker compose pull anycable-go && docker compose up -d 即可;anycable-go 无状态,客户端断线重连由前端 SDK 负责。
七、常见问题(FAQ)
Q1:客户端能连上 WebSocket,但订阅任何频道都无响应/被拒?
A:检查 RPC 容器是否存活:docker compose ps rpc;再查 anycable-go 日志是否反复出现 grpc connection is not ready。绝大多数情况是 RPC 容器崩了或 ANYCABLE_RPC_HOST 写成了 127.0.0.1(容器内环回指向自己,必须写服务名 rpc)。
Q2:HTTP 广播返回 401?
A:请求头缺少 Authorization: Bearer <ANYCABLE_BROADCAST_KEY>,或密钥与 anycable-go 环境变量不一致。
Q3:Ruby 应用广播了但客户端收不到?
A:确认 AnyCable.broadcast("chat", JSON.generate({...})) 的 data 是 JSON 字符串而非 Ruby 对象;对象形态会被序列化成 anycable-go 不认识的结构而静默丢弃。
Q4:docker compose up 时 anycable-go 反复重启?
A:多半是 Redis 未就绪。本文 compose 已用 healthcheck + condition: service_healthy 规避;如果你删减了这段,anycable-go 会在 Redis 起来前订阅失败退出,靠 restart: unless-stopped 兜底也能自愈,只是日志难看。
Q5:如何确认 RPC 与 anycable-go 版本配套?
A:anycable-go 看镜像 tag;Ruby 侧 docker exec anycable-1.5.6-rpc anycable --version。同代规则:anycable-go 1.5.x ↔ anycable gem 1.5.x。
八、附节:Kubernetes 部署形态(官方方案,未实测)
AnyCable 官方提供 Helm Chart 与 k8s 部署指引(来源:docs.anycable.io/deployment/kubernetes 与 github.com/anycable/anycable-helm/releases,查询日期 2026-10-25)。k8s 集群落地实测将在后续 K1 批次专栏中呈现,本节仅梳理官方推荐形态。
形态对比:
| 维度 | Docker Compose(本文主线) | Kubernetes(官方 Helm Chart) |
|---|---|---|
| 适用规模 | 单机/小团队 | 多副本、弹性伸缩、滚动发布 |
| anycable-go 部署 | 单容器 + ports 映射 | Deployment(多副本)+ Ingress 终结 /cable |
| RPC 后端 | 单容器 | 独立 Deployment + ClusterIP Service(50051) |
| 广播总线 | 容器内 Redis | 集群内/外部 Redis(Secret 管理 URL) |
| 配置注入 | environment 明文 | values.yaml → env / Secret |
官方 Helm Chart 要点:
- Chart 仓库:
https://helm.anycable.io/,chart 名anycable-go,最新版本0.5.11(支持 anycable v1.4+)。 - 作为应用 chart 的依赖引入:
# Chart.yaml(Helm 3)
dependencies:
- name: anycable-go
version: 0.5.11
repository: https://helm.anycable.io/
- 关键 values(官方文档示例):
anycable-go:
env:
anycableRpcHost: anycable-rpc:50051 # 同命名空间 RPC Service
anycableRedisUrl: redis://:<密码>@redis-anycable:6379/0
ingress:
enable: true
path: /cable
- RPC 后端在 k8s 中需自建:Deployment(
command: [bundle, exec, anycable, --rpc-host=0.0.0.0:50051])+ ClusterIP Service(端口 50051),并可加 NetworkPolicy 只允许 anycable-go Pod 访问 RPC。 - anycable-go 无状态,chart 0.5.6 起支持 HPA 自动扩缩容;广播经 Redis 天然跨节点,无需会话亲和。
⚠️ 本节内容均引自官方文档与 Helm Chart 发布页,未在真实 k8s 集群实测;完整的 k3s/k8s 部署、Ingress TLS、HPA 验证见后续 K1 批次文档。
九、参考链接
- AnyCable 官方文档(Docker 部署):https://docs.anycable.io/deployment/docker
- AnyCable 官方文档(Kubernetes 部署):https://docs.anycable.io/deployment/kubernetes
- 官方 Helm Chart:https://github.com/anycable/anycable-helm
- anycable-go 仓库与 Release:https://github.com/anycable/anycable-go
- anycable-go Docker Hub:https://hub.docker.com/r/anycable/anycable-go
- 本专栏关联文档:《Ubuntu 22.04 安装 AnyCable-Go 1.5 指南》《AnyCable 1.5 配置参数详解》