跳到主要内容
拾光SHIGUANG JOURNAL

wiz 外置 Redis 一键脚本

By Heanny
2026-11-15
1 read

两个容器明明在同一个 Docker 网络,为什么 Redis 还是连不上?

一次从「加个网络就行」到翻编译配置、再到写一个能重复执行的运维脚本的过程。

最近遇到一个看起来很简单的需求:某个一体化部署的应用镜像自带了一个旧版 Redis,而机器上已经跑着一个独立的新版 Redis 容器。想让应用改用外面那个,顺便把内置的老版本停掉。

我第一反应是:这有什么难的,把两个容器放到同一个网络不就行了?

结果这个「显然」的答案,让我绕了一整圈。


一、先说结论:网络从来不是问题

两个容器本来就在同一个自定义网络里,端口直连测过是通的,容器名也能正常解析。真正卡住的是应用配置里那一行:

"redis": { "as": { "host": "127.0.0.1", "port": 6379 }, "ks": { "host": "127.0.0.1", "port": 6379 } }

127.0.0.1 在容器里指向的是容器自己。那里跑的正是镜像自带的旧 Redis。网络再通,应用也只会连自己的 loopback。

这是容器网络里特别容易踩的一个认知错位:「能不能到达」和「连的是谁」是两件事。加网络解决的是前者,而这里出问题的是后者。

更要命的是,我当时已经顺着「应用地址改不了」这个假设,写了一整套方案:在容器里跑一个 TCP 转发进程,把本地 6379 透传到外部 Redis,再改启动脚本、挂进 compose。代码都验证能跑了。

直到被问了一句:「两个容器不是已经在同一个网络吗,为什么要透传?」


二、别信 strings,把配置加载出来看

我最初的判断是「host 写死在代码里,改不了」。这个判断来自对编译配置的表面扫描:应用服务端配置是 bytenode 编译过的 .jsc 文件,strings 只能看到一堆零散的键名,看不出 redis 段里到底有没有 host。

后来换了个思路:既然是 Node 的字节码,那就直接用 Node 把它加载出来。

docker exec -w <服务端目录> -i <容器> node <<'JS' require('bytenode'); const c = require('./config/default.jsc'); console.log(JSON.stringify(c.redis, null, 2)); JS

拿到的是普通 JavaScript 对象,host 字段清清楚楚。所谓「写死」,只是我看了半张图就下的结论。

接着要确认「改哪里才生效」。继续加载 common/wizconfig.jsc,会发现它用 lodash/merge 把 /wiz/storage/config/overwrite.json 深度合并进默认配置。深度合并意味着不用整份重写,只补一个 redis 段就能覆盖 host,其它配置原样保留:

{ "redis": { "as": { "host": "redis", "port": 6379 }, "ks": { "host": "redis", "port": 6379 } } }

用 Docker DNS 的容器名当 host,连 IP 都不用硬编码。

教训有两层:一是编译过的配置不等于不可读,能 require 就别靠猜;二是「看起来改不了」和「真的改不了」之间,往往只差一次实际加载。


三、三种方案,选改动最小的那个

方案 做法 代价
容器内端口转发 常驻进程把 127.0.0.1:6379 透传到外部 多一个进程、要改启动脚本、升级后容易失效
改配置直连 overwrite.json 覆盖 redis.*.host 一个 JSON 段,零额外进程
只加同一个网络 什么都不改 无效,问题根本不在网络

最后还补了一步:既然应用已经不连内置 Redis,就把启动脚本里的 redis-server 也停掉,省得它空转。这一步需要改 entrypoint 并挂载进容器,是整个方案里唯一「动到镜像行为」的地方。

所以最终是三步:改配置 → 生成并挂载启动脚本 → 重建容器。笔记正文在数据库里,Redis 只放会话和缓存,切换成本就是客户端可能要重新登录一次。


四、真正花心思的,是让脚本「能重复跑」

一次性改完很容易。麻烦的是下次镜像升级:新镜像的 entrypoint 可能换了写法,compose 可能被覆盖,配置文件可能被重置。人肉记「升级后要做哪三件事」迟早会漏。

于是把脚本设计成幂等的,核心是四个决定:

1)从镜像导出,而不是从运行中的容器导出。

这是最容易写错的一处。运行中的容器已经挂载了补丁脚本,docker cp 出来的是「改过的版本」;拿它再打一次补丁,内容会自我累积,慢慢和真实镜像脱节。正确做法是从镜像本身取原始文件,而且不需要启动服务:

CID=$(docker create <镜像名>) docker cp "$CID:/<镜像内>/entrypoint.sh" ./entrypoint.raw.sh docker rm -f "$CID"

2)用镜像 ID 做标记,而不是时间戳。

生成的脚本头部写一行 # [wiz-redis] image=<镜像ID> host=<目标>。镜像没变,生成内容就逐字节一致,脚本检测到「无需变更」直接零中断退出;镜像一升级,ID 变了,自动重新导出、重新打补丁。时间戳做不到这一点——每次跑都会认为文件变了,白白重启服务。

3)补丁要抗版本变化,不能写死文本块。

最初版本是精确匹配一段 if/else/fi 再整体替换,镜像一改注释就会失配。改成逐行规则:凡是非注释的 redis-server 启动命令,替换成 true 占位(这样 if/else/fi 结构依然合法);凡是指向本机 6379 的地址,不管写成 127.0.0.1:6379、localhost:6379 还是 -h 127.0.0.1 -p 6379,一律改向外部 Redis;如果没有等待 Redis 就绪的语句,就补一条。

4)生成后先校验,再落盘。

bash -n 检查语法,再确认没有任何残留的内置 Redis 启动行,两项都过才写文件;否则安全中止,现有脚本不受影响。另外写入必须用原地截断(open(path, 'w'))—— 单文件 bind mount 是靠 inode 生效的,sed -i 之类换 inode 的写法,容器内看到的还是旧内容,得重建容器才刷新。

compose 挂载行和 overwrite.json 的 redis 段也都做幂等检查:被覆盖了就自动补回,全就绪就什么都不做。只有真有变化才 docker compose up -d 或 docker restart。


五、两个值得记下的坑

只读挂载触发 chmod 警告。 原启动脚本里有一句给自身加执行权限的 chmod,挂载成 :ro 后会报 Read-only file system。其实镜像的 CMD 是 bash <脚本>,根本不需要执行位,把那行注掉即可。

回滚必须成对。 只把 overwrite.json 还原,应用会改回连 127.0.0.1,但挂载的启动脚本已经停掉了内置 Redis —— 结果是两边都连不上。必须 compose 和配置文件一起还原并重建。我把这条写进了脚本执行结束自动打印的回滚命令里。


六、验证要看「证据」,不要看「没报错」

脚本每次跑完会输出一组可核对的事实:应用日志里 redis client is ready 指向哪个地址、内置 Redis 进程是否存在、loopback 端口是否还在监听、外部 Redis 的版本和连接数、相关错误日志条数。

另外专门做了一个 --check 模式:只体检、不写文件、不重启,把「哪些项需要更新、预计会执行什么动作」打印出来。升级前先看一眼,比直接跑放心得多。我还用它模拟过两种情况——新版镜像换了启动脚本写法、以及 compose 挂载和配置被一起重置——都能正确识别且不动文件。


七、小结

这件事技术含量不算高,但很典型:

  • 先别急着写方案,确认问题到底出在哪一层。网络通不通、地址指谁、配置能不能改,是三个不同的问题;
  • 遇到编译/压缩过的配置,能加载就别靠字符串猜;
  • 运维脚本的价值不在「这次跑通」,而在「下次升级还能跑」,幂等设计比一次性补丁更值钱;
  • 改动越小越好。最后落地的是一个 JSON 段 + 一份挂载脚本,没有新进程、没有端口转发、没有装任何东西。

完整脚本和使用文档见配套文件,所有环境相关值都抽成了配置区变量,可直接在自己的环境里改。


配图建议(发布时可补)

  1. 网络关系图:两个容器在同一 Docker 网络,箭头分别指向「容器自身 loopback」和「外部 Redis」,直观说明为什么加网络没用。
  2. 方案对比图:透传 / 改配置 / 只加网络三条路径,标出额外进程与改动量。
  3. 幂等流程图:镜像 ID 比对 → 有变化则重新导出 → bash -n 校验 → 落盘 → 按需重启;无变化则直接退出。

初稿状态:文中路径、主机名、端口均为占位,可按实际环境替换;如需补充具体命令清单或跑一次真实环境的输出截图,可再加一节「实操演示」。

ESP-01S的一些使用

读者评论

暂无评论,还不快来坐沙发...

留下你的想法

评论和回复需要登录。登录后请再次确认提交。