上一篇讲了 99 元 ECS 从装 Docker 到上线的踩坑单(见《99 元 ECS 上线踩坑单》),这篇讲点更底层的东西:容器跑起来之后,围绕 compose 文件、挂载、命名、重建行为发生的四件事。素材全是 10 月 21 日当晚的操作记录,改了什么、为什么改、怎么验收,都有据可查。
机器现状先交代:2 核 2G,/root/app 下 docker compose 起两个容器——nginx(80/443,反代 + 静态博客)和 node-app(Nuxt3 后端,容器内 3000)。
一、bind mount:冒号左右是两个世界
compose 里最常被我忽略又最重要的一行:
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
冒号左边是宿主机路径(SSH 里 ls 得到的),冒号右边是容器内路径(docker exec 进去才存在的)。 /etc/nginx/conf.d 这个目录在服务器上根本不存在——它只活在 nginx 容器那个独立的小文件系统里,物理内容就是宿主机的 /root/app/nginx/conf.d 被 bind mount 投射进去。同一块磁盘,两套门牌号。
nginx 配置里写 root /usr/share/nginx/blog 时,进程是按容器视角找文件,找到的实际是宿主机的博客产物目录。这解释了一个我此前觉得神奇的现象:scp 上传新博客后不用重启任何东西,刷新就是新内容——nginx 每次请求都现去容器内路径读文件,而那个路径实时映射到刚落盘的宿主机目录,中间没有任何复制或缓存环节。
两个配套习惯:
- 挂载尾部加
:ro(只读)。我的证书目录、博客目录、conf.d 全是 ro——容器最多读到,写不了,误操作被内核直接挡下。 - 想确认映射关系,别猜,一条命令看全量表:
docker inspect nginx --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
二、目录重组:按容器分家,顺手收一个安全事故
当晚 22 点把 /root/app 从"一锅炖"重组成按容器分目录:nginx/(conf.d、blog、certbot-www、certbot)、node-app/(构建产物 output)、scripts/、archive/。动机一半是整洁,一半是安全——node-app 原来的挂载是 .:/app,等于把整个 /root/app 塞进容器,包括存着数据库密码和各种 API key 的 .env。收窄成 ./node-app:/app 后,容器只看得到自己的代码。
改挂载路径的连锁清单,少一个就断:compose 里 nginx 四个挂载同步改 ./nginx/*;证书续期 cron 指向新位置的 certbot_renew.sh;脚本内的证书/webroot 路径 sed 换新前缀;本地部署脚本的 scp 目标目录也要改。
动手前留了两条退路:77M 全量 tar 到 /root/backup/,旧 compose 存进 archive/。验收不看"容器 Up"就完事,而是逐条打流量:IP:80 = 200、博客 https = 200、需鉴权接口 = 401(说明反代通了且鉴权逻辑正常)、检索预热日志正常。重组这种全量挪目录的操作,值得一次全量备份,成本 77M,买的是"改崩了五分钟回滚"。
三、容器命名:服务名、container_name、网络别名是三回事
compose 不写 container_name 时,容器默认名是 <项目>-<服务>-<序号>——项目名取目录名,所以我那台机器上 nginx 容器叫 app-nginx-1。能跑,但丑,而且脚本里引用它就得背下这个拼装规则。
修法就是显式钉死:
services:
nginx:
container_name: nginx # docker ps 里就叫 nginx
当晚改名踩到的最有价值的坑在这里:容器名会被硬编码进外围脚本。nginx 从 app-nginx-1 改成 nginx 之前,先 grep 了全服务器的引用——果然在证书续期脚本里躺着一行 docker exec app-nginx-1 nginx -s reload。不同步改掉,下次 cron 续期就静默失败。所以我的动作顺序是:备份 compose → 加 container_name → 全局 grep 旧容器名逐个改 → docker compose up -d nginx(改 compose 本身会触发重建,无需 force)→ 三站点 200 验收。
另一个容易混的点:容器间互相访问用的是服务名,不是容器名。nginx 反代配置里写 proxy_pass http://node-app:3000,这个 node-app 是 services 下的键名,compose 网络会自动把它解析成容器 IP;container_name 只影响 docker exec/docker ps 这些宿主机动词。分清这两层,改名才改得不慌。
四、compose up 的"懒得动"行为:改了代码不等于改了容器
固化部署流水线(本地构建 → tar → scp → 服务器解压 → docker compose up -d --force-recreate node-app)时撞的坑:不加 --force-recreate 的 up -d,如果 compose 配置本身没变,它看到容器已经是 Running 就直接跳过——新代码解压在挂载目录里,但容器内存里跑的还是旧进程。表现是页面 title 改了、线上没变化,一度以为构建出错。
- 挂载卷改"文件内容"(博客 html、配置):不用动容器,实时生效;
- 改 compose 配置(镜像/环境变量/挂载):
up -d会自动重建对应容器; - 改了代码要重启进程但 compose 没变:
up -d --force-recreate或restart。
三种"变更"对应三种生效方式,混着记就乱。我的流程里干脆把 --force-recreate 写死进部署脚本,并在同一步里抓线上 HTML 的 <title> 和探一个鉴权接口返回 401,作为"新构建真的在服务"的证据——entry hash 变了才算数。
五、反代与限流:两域名一台机的最终形态
22 点半给 node 应用单独开了 node.zjkun.top,A 记录指到同一台 ECS,nginx 按 Host 分流。新域名的 80 只留 ACME 验证路径、其余 301 上 https;443 反代 node-app:3000。几个关键配置都有明确理由:
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
反代天然会把访客身份弄丢——后端日志里只见 docker 网关 IP、生成的回调 URL 拼成 http。这四行就是把真实 IP 和协议原样递进去。
proxy_read_timeout 300s;
proxy_buffering off;
这两行是为 SSE 流式接口准备的:默认 60 秒收不到字节 nginx 就掐线报 504,AI 应用动辄生成一两分钟;默认攒块转发则打字机效果出不来,关缓冲来一点推一点。
限流用两个 limit_req zone:登录类接口(正则覆盖 /api/login、注册、token 等)1r/s、burst=5,防爆破;全站 10r/s、burst=20、nodelay,兜刷量;limit_req_status 429。实测连打 12 发登录,第 6 发起全部 429,同时首页照常 200——严区和宽区各管各的。证书方面新域名由既有的每日 04:30 certbot cron 自动覆盖(renew 本来就扫全部证书),不用新加任务。
验收清单(当晚全绿)
| 项 | 结果 |
|---|---|
| 容器 | nginx、node-app 双 Up,命名已钉死 |
| IP:80 / 博客 https / node https | 全部 200 |
| 需鉴权接口 | 401(反代通、鉴权在位) |
| 限流 | 登录连打 12 发,第 6 发起 429 |
| 引用同步 | 续期脚本、cron、本地部署脚本无残留旧路径/旧容器名 |
| 回滚 | 全量 tar + 旧 compose 存档 |
一天下来最大的体会:Docker 的很多"玄学",拆开都是三层问题——文件在哪(挂载的两套门牌号)、谁在引用它(硬编码的容器名和路径)、变更怎么生效(文件级/配置级/进程级)。 改前先备份、grep 引用,改后按流量验收,基本就不会翻车。
本文基于 2026-10-21 当晚服务器实操记录整理,操作明细与回滚存档位置见文中对应小节。