AnyCable-Go 1.5 Docker Compose 部署指南

用 Docker Compose 一键编排 anycable-go 1.5.6 + Redis 7 + Ruby RPC 完整实时推送栈,完成 WS 订阅、HTTP 广播、Redis 广播三链路端到端实测,附官方 Helm Chart 的 Kubernetes 部署形态。

AnyCableAnyCableWebSocketDockerDocker Compose2026/10/25

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)
  • 宿主机端口分配(避开本机已占用端口后选取):
宿主机端口容器端口用途
81398080WebSocket /cable 与健康检查 /health
81408090HTTP 广播端点 /_broadcast
81419091Prometheus 指标 /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_KEYHTTP 广播鉴权密钥不设则广播端点裸奔;演示值 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 批次文档。

九、参考链接