生产事故复盘:从 rsync 绕路到 Docker 端口裸奔

换了一台电脑,部署崩了。页面白屏,样式全毁,/_next/static/ 全线 404。 表面是 nginx 配置问题,实际是一次从部署流程到安全架构的全面溃败。

事故时间线

  1. 阶段一:rsync 绕路 — 为了快速部署,我用 rsync 直接把文件推到服务器,绕过了 git。服务器跑起来了,但 git 仓库里的代码是旧的。
  2. 阶段二:换电脑崩溃 — 换了一台电脑,从 git 拉代码重新部署。git 里存的是有 bug 的旧版 nginx.conf(alias 指向不存在的目录),服务器上 rsync 推的正确版本被覆盖了。
  3. 阶段三:端口全裸奔 — 排查过程中发现 web(3000)、api(3001)、db(5432) 三个容器全部绑了 0.0.0.0,PostgreSQL 5432 端口对公网完全开放。
  4. 阶段四:nginx alias 炸弹 — git 里的 nginx.conf 把 /_next/static/ 指向 /var/www/next-static/,这个目录根本不存在。所有 JS/CSS 全线 404。

根因一:rsync 绕过 git

这是最深的根因。rsync 能快速同步文件,但它不经过版本控制。 服务器上跑的是 rsync 推的正确版本,git 里存的是有 bug 的旧版本。 换电脑后 git pull 回来的是旧版本,一行 git 就把所有修复覆盖了。

铁律:所有代码变更必须通过 git push → 服务器 git pull → docker compose build 流程部署。禁止 rsync 绕路。

根因二:Docker 端口全暴露

docker ps 显示三个容器的端口全部绑在 0.0.0.0 上:

services:
  web:
    ports:
      - "3000:3000"     # 绑了 0.0.0.0,全世界能扫到

  api:
    ports:
      - "3001:3001"     # 同上

  db:
    ports:
      - "5432:5432"     # PostgreSQL 裸奔公网!

这意味着全世界都能 nmap 扫到你的 PostgreSQL。 不需要密码爆破,端口本身就是信息泄露——攻击者知道你跑了数据库。

修复:Docker internal 网络隔离,只有 nginx 对外。

services:
  web:
    networks:
      - internal       # 不映射端口,只内网

  api:
    networks:
      - internal

  db:
    networks:
      - internal

  nginx:
    ports:
      - "80:80"        # 唯一公网入口
      - "443:443"
    networks:
      - internal

networks:
  internal:            # Docker 内部网络隔离

修复后 docker ps 只显示 nginx 的 80/443, web/api/db 的端口在容器网络内部通信,外部完全不可达。

根因三:nginx alias 炸弹

nginx.conf 把 /_next/static/alias 指向了一个不存在的目录。 结果所有 JS/CSS 请求返回 404,页面白屏。

# 炸弹:指向不存在的目录
location /_next/static/ {
    alias /var/www/next-static/;   # 404!
}

修复:改用 proxy_pass 代理到 Next.js standalone。

# 修复:代理到 Next.js standalone
location /_next/static/ {
    proxy_pass http://web;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

根因四:镜像臃肿

旧镜像 778MB,因为它打包了完整的 node_modules。 Next.js standalone 模式只打包运行时必需的文件。

# Production — standalone 模式
FROM node:20-alpine AS runner
WORKDIR /app

# standalone 服务端
COPY --from=builder /app/apps/web/.next/standalone ./
# 静态资源(必须手动 COPY)
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
# public 目录
COPY --from=builder /app/apps/web/public ./apps/web/public

WORKDIR /app/apps/web
CMD ["node", "server.js"]
模式镜像大小
非 standalone(打包 node_modules)778 MB
standalone(运行时精简)186 MB

减重 76%。standalone 的核心思路:server.js + .next/static + public, 三件套就是全部运行时依赖。不需要 node_modules,不需要 devDependencies。

隐患清单(已修复)

隐患风险修复
rsync 绕过 git换电脑后覆盖修复只准 git push → pull → build
3000/3001/5432 裸奔公网可扫描数据库端口Docker internal 网络隔离
nginx alias 炸弹全线 404 白屏改用 proxy_pass
镜像 778MB启动慢、传输慢standalone 186MB(-76%)
/stats 无密码流量数据泄露nginx Basic Auth

教训

每一个环节都不是高深的技术问题——rsync 绕路、端口暴露、alias 指空、镜像臃肿——但叠在一起就构成了生产级灾难。 安全的本质不是加多少层防护,而是确保每一层的地基是稳的。

最深的一条:部署流程本身就是安全防线。 rsync 绕过了 git 这个版本控制的防线,让后续一切修复都变成了沙滩上的城堡。 换电脑只是触发器,真正的裂缝早就存在了。

在线体验:上面提到的 standalone 架构和 Docker 网络隔离,正在为 AudioWorklet 实时探针服务——

打开 Live Lab →

2026-04-15 · 生产安全复盘 · diffserv.xyz