☰
开源网关Claw云服务器部署:Docker Compose与HTTPS实践
2026/10/21 12:27:06 网站建设 项目流程

“买龙虾”这个栏目名,是我随手起的,没什么讲究,就是想表达“把东西搞到手、折腾明白”那股劲儿。第一期要折腾的,是一个在代码托管平台上小有名气的开源网关项目,内部代号就叫 Claw。它能做的,是把各种 Webhook、定时任务、消息推送和内部接口统一收口到一个可视化控制台里,相当于给你那堆散落各处的自动化脚本装了一个总闸。我把部署环境放在国内某主流公有云的标准云服务器上,机器不算新,2核4G,跑起来刚刚好。

这篇文章既是我这次部署过程的完整记录,也算是给打算自己上手跑类似项目的人一份参考。我会把从选机器、装环境、写编排文件、配域名证书,到后面踩坑排查的整个过程都摊开讲,哪些地方容易翻车,哪些参数要提前想清楚,都会说透。

1. 为什么第一期选它:一个“网关类”开源项目

1.1 这个小工具到底能干什么

很多人第一次看到 Claw 这类项目,第一反应是“这不就是个消息转发工具吗”。我一开始也这么想,后来真正用起来才发现,它的核心价值不在转发,而在“收口”。

举个例子。你手上有好几个自动化流程:一个监控网页价格变化,一个接收服务器告警,还有一个每晚会跑数据统计然后往群里发结果。正常情况下,这三件事分别由三个脚本完成,彼此之间没有关联,报错方式也各不相同。价格脚本挂了可能没人知道,服务器告警发到钉钉,统计结果发到邮件,管理起来非常零散。

Claw 做的事情,就是把这些“入口”全部接到自己身上。它内部维护一套触发器机制,支持标准 Webhook 接入,也支持定时轮询和队列消费。外部服务只要往它提供的地址发一个请求,它就能根据你配置好的规则,把消息做格式化、过滤、聚合,再分发到你指定的目标——无论是某个群机器人、邮件、还是另一套内部系统。同时它自带一个还算清爽的 Web 控制台,所有触发记录、分发日志、失败重试状态,都能在浏览器里直接看到。

这个定位有点像家里进水管的“总阀”:以前每个水龙头后面都接一根水管,现在所有水管先接到总阀,再由总阀统一分路。好处是显而易见的:排查问题的时候不需要挨个脚本翻日志,对账的时候也有了一本统一的流水账。

1.2 云主机选型:2核4G够不够,怎么算

选定项目之后,第一个现实问题就是:拿什么样的机器来跑它。

Claw 这种项目,底层依赖 Docker 容器,内部至少会拆出前端静态服务、后端 API、数据库三个容器,如果开了队列功能,还可能再加一个消息中间件。把所有容器算在一起,内存占用通常在 1GB 到 1.5GB 之间。CPU 平时基本吃不满,但遇到瞬时大量 Webhook 请求到来时,单核会明显吃紧。

所以 2核4G 这个配置,对它来说不是“刚好够用”,而是“留了一截余量”。1核2G 的机器其实也能启动,但一旦并发上来或者数据库开始做比较重的查询,系统负载会直接飘红,容器可能因为内存不足被 OOM Killer 杀掉,表现为服务无端重启。4G 内存的好处在于,就算数据库和消息中间件同时跑起来,系统仍然有将近 2GB 的可用内存,docker compose 里的 healthcheck 也能稳定通过。

带宽方面,我选的是 5Mbps 按固定带宽计费。为什么不是按流量计费?因为这个项目的使用场景以 API 请求为主,消息体量很小,日常带宽占用极低,按流量计费看似灵活,实际长时间开着反而更容易产生意外账单。5Mbps 的固定带宽足够支撑几十个 Webhook 并发接入,峰值也够用。

操作系统我选了 Ubuntu 22.04 LTS。没有选更新的 24.04,原因很朴素:22.04 的软件仓库、内核版本以及各大软件源的兼容性都经过了充分验证,Docker 和 Compose 插件在上面安装最省事。很多时候,部署翻车不是项目本身有问题,而是系统太新,依赖还没跟上。

2. 部署方案背后的设计思路

2.1 项目内部结构拆开看

把 Claw 这个项目拉开看,其实是一个典型的前后端分离结构,再加上配套基础设施。

前端部分是一个单页应用,负责呈现控制台界面。它本身不直接访问数据库,所有操作都通过调用后端暴露的 REST API 完成。这样做的好处很明显:控制台只是“遥控器”,真正的业务逻辑全在后端,前端挂了不影响已配置的触发规则继续运行。

后端服务是整个项目的核心。它承担着三块职责:对外接收并校验 Webhook 请求、读取和写入配置数据、把消息按规则投递到目标渠道。由于这几块职责在代码层面是分开的,所以它天然适合做成无状态服务,多开几个副本也不会出现数据错乱。这也是为什么它可以被塞进容器里跑——容器化的前提就是服务本身不依赖宿主机上的特殊环境。

数据层用的是 PostgreSQL 加 Redis 的组合。PostgreSQL 存的是用户配置、触发记录、投递日志这类“必须保证不丢”的数据;Redis 存的是队列、限流计数这类“丢了也能重建”的临时数据。两个库职责分明,备份策略也因此可以区分:PostgreSQL 做每日全量备份,Redis 只做持久化兜底。

队列组件是可选模块。如果你的使用场景只是“收到 Webhook 就立刻转发”,那消息中间件完全可以不装。但如果你需要失败重试、削峰填谷、批量聚合,队列就派上用场了。这套设计思路在同类项目里很常见:核心路径保持简单,扩展能力留给可选模块。

2.2 为什么用 docker compose 而不是其他方式

确定用 docker compose 而不是裸机部署,也不是无脑跟风,主要基于三点考量。

第一是依赖隔离。Claw 的后端对运行环境有具体要求:Node.js 版本不能太旧、Python 解释器版本要匹配、系统库要装齐。如果直接装在宿主机器上,今天装一个依赖,明天升级一个库,很容易把系统环境搞乱。容器把应用和依赖一起打包,换机器部署时只要把镜像拉下来跑,环境一致性天然有保障。

第二是一键编排。现在这类项目基本都提供了现成的 docker-compose.yml,里面把前端、后端、数据库、队列的启动顺序和网络都定义好了。我只负责填环境变量,剩下的事情交给 Compose 处理。相比手动一行一行敲命令启动进程,这种方式更接近“声明式”管理:我要什么状态,配置文件里写清楚,剩下的交给工具。

第三是回滚方便。裸机部署如果升级失败,想回到旧版本只能靠备份或者操作系统的包管理工具,过程很痛苦。容器方案就简单得多:本地留着旧镜像,新版本出问题就用旧镜像重新把容器拉起来。整个回滚过程在一分钟以内。

当然,没有万能的方案。如果这台机器上要运行几十个服务,或者要支撑弹性伸缩,那 docker compose 就不够用了,该上 K8s 还是得上。但单机跑这种体量的项目,K8s 属于杀鸡用牛刀,光是控制平面的内存占用就能压垮 4G 的小机器。

3. 动手前的基础环境准备

3.1 SSH 登录与系统初始化

拿到云主机之后,第一件事不是装 Docker,而是把系统基础配置好。

我用 SSH 以 root 用户登录,首先做的是更新软件源。这个步骤很多人会跳过,但它其实很有必要——新买的机器,镜像里的软件源可能指向的是某个固定版本快照,不更新的话,后续装包可能装到旧版本。

更新完之后,我立刻创建了一个普通用户,并给它加了 sudo 权限。养成不用 root 直接操作的习惯,是很重要的一层防线。平时登录服务器使用这个普通用户,只有需要执行系统级命令时才临时用 sudo,能减少很多因为手滑导致的问题。

apt update && apt upgrade -y adduser claw usermod -aG sudo claw

接下来是登录方式的调整。云主机的默认登录方式通常是密码登录,如果你在创建机器时设置了密钥,那这条可以跳过;如果用的是密码,建议尽快把本地的公钥拷贝到服务器的 authorized_keys 文件里,然后改为密钥登录。密码登录最大的风险是暴力破解,哪怕你把密码设成 30 位随机串,也架不住别人用字典一批一批地试。

ssh-copy-id claw@<服务器IP>

改了密钥登录之后,我把 SSH 服务的密码登录选项关掉了。这一步很多人不敢动,觉得万一密钥丢了就连不上机器了。但实际上,只要你本地保存好私钥,并多做一份备份到加密U盘或者密码管理器里,比每天都开着密码登录要安全得多。

网络层的防火墙我选择了 ufw,规则很简单:放行 SSH 端口、80 和 443,其余入站一律拒绝。注意,如果你用的云主机开启了安全组,那云平台层面的安全组和系统内部的 ufw 是两层独立的东西,只有同时配置好才算完整。

3.2 安装 Docker 与 Compose 插件

装 Docker 这条路,我建议大家优先用官方脚本,但前提是你的网络能顺畅访问相关源。如果访问不畅,就改用系统自带仓库里的 docker.io 包,虽然版本可能旧一点,但稳定够用。

我在这次部署中使用的安装方式是通过系统自带的 apt 仓库安装,之后再单独补装 Compose 插件。之所以不用官方脚本,是因为官方脚本会添加额外的软件源,在某些网络环境下反而会把事情变复杂。

apt install docker.io docker-compose-v2 -y

装完以后验证一下版本:

docker --version docker compose version

看到版本号正常输出来,基本就说明 Docker 环境已经就绪。接下来做两个收尾动作:把 claw 用户加入 docker 组,这样部署时不需要每次都在命令前面加 sudo;然后设置 Docker 开机自启。

systemctl enable docker usermod -aG docker claw

这里有一个细节:docker 组权限等同于 root。加入 docker 组的用户,完全可以通过操纵容器来拿到宿主机权限。所以,如果你和他人共用服务器,不要轻易把别人加入 docker 组,这是安全上的一个重要边界。

3.3 镜像拉不下来怎么办

第一次执行 docker compose up -d 时,我碰到了几乎所有国内云主机用户都会遇到的问题:镜像拉取速度很慢,甚至卡住不动。

这跟网络链路有关,也和 Docker Hub 的访问策略有关,属于正常现象。解决办法是给 Docker 配置镜像仓库地址,也就是 registry mirror。具体操作是修改 /etc/docker/daemon.json,把镜像拉取请求分流到可用的镜像源上。

{ "registry-mirrors": ["https://docker.mirrors.example.com"] }

这里有个容易踩的坑:不同云厂商提供的镜像源地址不一样,而且这些地址可能会因为运营策略调整而变化,不建议直接照抄网上的某个地址,最好去你所使用的云平台文档里查一下当前可用的镜像仓库地址。配好之后重启 Docker:

systemctl restart docker

再拉镜像,速度通常会快很多。

另外提醒一句:镜像源只影响镜像拉取这个环节,容器运行时的网络仍然正常,不会对后续请求产生什么负面影响。

4. 核心部署流程:从目录到服务跑通

4.1 目录规划与配置文件

环境搞定之后,我开始搭项目目录。这里我的习惯是先把目录结构规划好,再写配置文件,避免后面东一个文件西一个文件,找起来麻烦。

我采用的目录结构是:

/opt/claw/ ├── docker-compose.yml ├── .env ├── data/ │ ├── postgres/ │ ├── redis/ │ └── uploads/ └── caddy/ └── Caddyfile

项目放在 /opt/claw 下面,而不是 root 家目录或者 /home 下,原因是 /opt 专门用于存放第三方软件,权限清晰,不容易和用户个人文件混在一起。数据量的目录单独划出来,所有容器产生的持久化数据都落在这里,将来做备份只需要打包这一个目录。

docker-compose.yml 是这个部署的核心文件。项目官方提供的版本通常已经定义好了所有服务,我做的事情是基于自己的实际情况做微调。关键配置项包括:镜像版本、端口映射、存储卷挂载、环境变量引用。

我这里用了一个值得推荐的小技巧:所有敏感配置,比如数据库密码、JWT 签名密钥,都不直接写在 docker-compose.yml 里,而是放在同目录下的 .env 文件中,然后在 compose 里用${VARIABLE}方式引用。好处是 docker-compose.yml 可以放心提交到代码仓库,而 .env 留在服务器本地,泄露风险大大降低。

4.2 启动服务与自检

配置写完之后,先用一条命令做配置检查:

docker compose config

这个命令会把你配置里的所有变量替换成实际值,然后输出完整的解析结果。如果语法有误或者引用变量不存在,会在这里直接报错,避免在真正启动时才暴露问题。

确认无误后,执行启动命令:

docker compose up -d

第一次启动需要拉取多个镜像,加上初始化数据库、安装依赖,整个过程会比较慢,耐心等即可。启动结束后,我习惯先看一遍容器状态:

docker compose ps

正常情况下,所有服务都应该是 running(运行中)状态。如果有容器显示 Restarting(重启中),基本可以断定是配置或者依赖出了问题,需要立刻看日志。

docker compose logs -f backend

日志里出现listening on 0.0.0.0:8080或者类似的提示,说明后端服务已经正常监听端口。接着再验证一下健康检查接口:

curl http://localhost:8080/healthz

返回ok或者{"status":"healthy"},基本就能确定整个后端链路是通的。前端控制台如果映射在 9000 端口,在本地浏览器访问一下,能正常打开登录页,整个部署就算完成了一半。

4.3 备份、升级与日常维护

服务跑通只是开始,更重要的其实是后面这些“看不见”的日常维护。我在这台机器上配置了两类备份。

第一类是数据库自动备份。PostgreSQL 数据是服务的命根子,我用系统自带的 cron 写了一条每日备份任务:凌晨 3 点执行 pg_dump,把数据导成 SQL 文件,再按日期压缩归档,保留最近 14 天的备份。

0 3 * * * cd /opt/claw && docker compose exec -T db pg_dump -U claw claw_db | gzip > /backup/pg_$(date +\%F).sql.gz

第二类是配置备份。docker-compose.yml、.env、Caddyfile 这几个文件体积很小,但价值极高,我直接用脚本打包同步到另一台存储机器上。这样即使整台云主机出问题,也能在新机器上快速重建环境。

升级流程方面,我总结出一套固定操作顺序:先备份数据库,再拉取新镜像,然后执行 docker compose up -d 让 Compose 自动重建配置有变化的容器,最后验证健康检查接口和页面状态。整个过程熟练的话五分钟内完成,相比裸机升级要省事得多。唯一的提醒是:升级前一定看一眼官方发布的版本说明,如果存在不兼容的配置变更,提前处理,否则容器可能反复重启。

5. 对外提供服务:域名、HTTPS 与安全加固

5.1 Caddy 反代与自动 HTTPS

服务在本地跑通了,但要让外部系统能够访问并调用,还需要通过域名对外提供入口。出于安全考虑,我坚决不建议直接对外开放 8080 这类端口,因为裸奔的 HTTP 服务既没有加密,也容易被扫描器盯上。

我使用的工具是 Caddy。相比传统的 Nginx 加 Certbot 组合,Caddy 最大的优势是自动申请和续期 HTTPS 证书。传统方式需要手动配置 SSL 证书路径,还需要额外写定时续期脚本;Caddy 只需要一行配置,它会自动向证书签发机构申请证书,到期后自动续期,完全不用操心。

我的 Caddyfile 是这样写的:

claw.example.com { reverse_proxy :9000 }

就这短短两行,Caddy 启动后就会自动为 claw.example.com 申请一张 HTTPS 证书,然后把公网进来的 443 流量转发到本机 9000 端口的控制台服务上。

这里值得注意的点是:域名解析需要提前在 DNS 服务商那里把 A 记录指向云主机的公网 IP,并且等待解析生效。如果域名没有正确解析,Caddy 在申请证书的阶段就会报错,因为证书签发机构需要确认“这个域名确实属于你”。

Caddy 的部署方式也很简单,我在 compose 文件里单独加了一个 Caddy 服务,把主机的 80 和 443 端口映射进去,再把 Caddyfile 挂载进容器。这样一来,所有进出流量都统一经过 Caddy,它既扮演了反向代理的角色,又顺带解决了 HTTPS,架构非常干净。

5.2 安全组和防火墙最小化

网络安全上,我一直坚持“最小权限原则”:能用到的端口才开放,用不到的端口一律关闭。

云平台的安全组和系统内部的 ufw 是两层防线,都要配置。云平台安全组我设置的规则如下:

协议端口来源用途
TCP22仅限我的办公网 IPSSH 登录
TCP800.0.0.0/0HTTP 跳转
TCP4430.0.0.0/0HTTPS 访问

系统内部的 ufw 也做了同样的配置。两层防火墙的好处是,即使某一层配置出错,另一层仍然能够挡住大部分非法访问。

数据库端口我完全没有对外开放。PostgreSQL 的 5432 端口只绑定在 Docker 内部网络上,外边即使扫描到这个端口,也无法直接连接。需要数据库操作时,我都是通过 docker compose exec 进入容器内部执行,这种方式在安全性上比开放远程连接要好得多。

同样不对外的还有 Redis 的 6379 端口。Redis 这类内存数据库如果暴露在公网上,且未设置密码,非常容易被恶意访问。我的原则是:凡是能被 Docker 网络内部互联访问的服务,一律不映射到宿主机端口。

5.3 端口暴露与密钥管理

再强调一次端口暴露原则,因为这个问题太容易出事了。

docker-compose.yml 里会看到两种端口写法:8080:8080和127.0.0.1:8080:8080。前者把端口暴露到所有网卡上,外部可以直接访问;后者只暴露到本机回环地址,只有服务器本机上的进程才能访问。如果你需要让容器端口仅供本机上的反向代理访问,一定要写明回环地址前缀,避免被公网直接访问。

密钥管理上,我强烈建议把敏感信息集中在 .env 文件里,并且权限设置成只有当前用户可读:

chmod 600 /opt/claw/.env

另外,环境变量里如果出现 JWT 密钥、API 密钥这类长期有效的凭证,建议安装一个密码管理器来存放备份,确保即使服务器完全丢失,也能恢复全部身份凭证。

6. 常见问题和调试实录

6.1 容器频繁重启

第一次部署时最容易遇到的现象就是容器反复重启,表现为 docker compose ps 里某个容器一直显示 Restarting,或者 STATUS 后面跟着(restarting)。

最常见的原因是内存配置不当。Claw 后端基于 Node.js 运行时,默认内存上限通常不设限制,但如果宿主机内存只有 2G,系统可能会在内存耗尽时强制杀掉容器。排查方法很简单:看日志。

docker compose logs backend

日志末尾出现FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory,基本可以断定是 Node 进程内存超限。解决办法是给容器显式设置内存限制,或者调大 Node 的堆内存上限。我后来在 compose 配置里加了mem_limit: 1g,重启容器,问题就再没出现过。

另一种常见情况是数据库还没初始化完成,后端就提前启动了,导致后端连接数据库失败而退出。这种情况日志里会显示connection refused。解决办法很简单:在 compose 配置里利用 healthcheck 让后端等待数据库健康后再启动,或者在启动脚本里加一段等待重试逻辑。

6.2 页面配置不生效

服务跑起来之后,还有一类“软故障”很让人抓狂:控制台页面明明加载出来了,但怎么改配置都不生效,刷新页面之后设置又变回原样。

第一反应是检查浏览器网络请求。打开控制台的开发者工具,看配置保存时提交的 API 请求是否返回了200。如果请求失败,说明问题在前后端接口层。如果请求成功但刷新后不生效,大概率是数据没有成功写入数据库,这时去查看后端日志,看看 SQL 语句执行是否报错。

还有一类隐蔽问题:时区不一致。容器默认时区是 UTC,而你的本机时间可能是东八区。如果你设置的定时任务是基于本地时间,但容器内部按照 UTC 计算,就会出现任务执行时间整体偏移 8 小时的情况。解决办法是在 compose 的环境变量里加上TZ=Asia/Shanghai,并重启容器。

我前后排查这个问题花了差不多半小时,最后发现罪魁祸首就是容器时区没设置。整个过程虽然有点浪费时间,但也让我记住了:凡是涉及定时调度的容器,第一时间先把时区变量加上。

6.3 证书申请失败

Caddy 自动申请证书失败,是另一个高频问题。

现象通常是:访问域名返回 ERR_CONNECTION_REFUSED 或者“不安全连接”,Caddy 日志里出现failed to obtain certificate。

排查顺序我建议这样来:

先在本地执行dig claw.example.com,确认域名解析已经指向服务器公网 IP。然后确认云平台安全组和系统防火墙是否真的放行了 80 端口。证书签发机构在域名验证阶段会访问你的 80 端口,如果这个端口不可达,申请必然失败。

再有就是时区问题在证书系统里的影响:Caddy 所在容器如果时间不准,向证书签发机构发请求时可能被认为请求无效。解决方法是确保服务器时间同步正常,并且容器内时间与宿主机一致。

最后一种情况比较冷门:你申请的域名之前被别人使用过,记录了某些异常的验证缓存,导致签发机构延迟确认。这种情况没什么好办法,只能等待或者换个临时域名测试。

把这三类问题理清楚之后,我发现它们都有一个共同特征:真正出问题的都不是项目本身,而是周边基础设施。网络不通看链路,应用起不来看日志,证书失败看端口,这些排查经验在别的项目里同样适用。务农的都知道,地里的庄稼长不好,多半不是种子的问题,而是土壤、水分和光照的条件不对。部署服务也是这个道理。我第一次折腾这类云上项目时,最深的体会就是:不要急着改代码,先把运行环境梳理干净,问题往往能解决一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询