docker 部署踩的 9 个坑:从 gcr.io 到健康检查

目标:https://rpc.myxingchen.xyz/api/echo 返回 JSON,延迟 < 10ms 链路:浏览器 → nginx(443) → rpc-proxy(Flask, 8080) → echo-server(C++, 9000) 最终:4.18ms HTTPS / 0.63ms 直连

把 RPC 框架扔上公网,听起来简单——docker build && docker run 完事。我踩了 9 个坑,4 个 deploy 晚上,反复搞。这篇文把每个坑的现象、根因、修法、教训完整记录,下次别再重复。

一、坑 1:gcr.io/distroless 拉不到

现象

1
2
ERROR: failed to solve: failed to fetch anonymous token: 
Get "https://gcr.io/v2/token?...": dial tcp 142.250.107.82:443: i/o timeout

根因

distroless 是 Google 维护的极简镜像,默认在 gcr.io 上。国内访问 gcr.io 不稳,有时通有时不通,IP 段被干扰。

修法

不用 distroless,改 debian:bookworm-slim——前者 ~20MB,后者 ~75MB,差 55MB 但100% 能拉

1
2
3
4
5
6
7
# before
FROM gcr.io/distroless/cc-debian12

# after
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends libprotobuf32 \
    && rm -rf /var/lib/apt/lists/*

教训

国内生产不用 gcr.io。要小镜像,用 alpine (但 glibc 兼容差) 或自建 registry mirror。bookworm-slim 是最稳的中庸选择。


二、坑 2:git pushbuild/ 也传上 GitHub

现象

git push 后 GitHub 仓库多出 200MB 的 build/ 目录——里面是 CMake 中间文件、.o、可执行文件。

根因

部署到 server 时,我在 server 上 clone → build → docker build——build/ 目录不该 commit,但 .gitignore 漏了

修法

  1. 立刻:git rm -r --cached build/ + commit
  2. 彻底:git filter-repo --path build/ --invert-paths + force push
  3. 预防:.gitignorebuild/,*.o,*.a

教训

任何用 make / cmake 的项目,第一件事是写 .gitignorebuild/ / *.o / .cache/ / compile_commands.json 都不该进仓库。


三、坑 3:cpp 文件用 uint32_t 但没 #include <cstdint>

现象

1
error: 'uint32_t' was not declared in this scope

根因

GCC 13 比 GCC 9/10 严格——uint32_t 必须显式 #include <cstdint>,以前能靠别的头文件间接 include,现在不行。

修法

Dockerfile 头注入:

1
2
3
RUN for f in $(find /src/src /src/examples -name '*.cc' -o -name '*.h'); do
    grep -q '#include <cstdint>' "$f" || sed -i '1i #include <cstdint>' "$f"
done

不优雅但work。正经修法是逐文件补,但 50+ 个文件太多了。

教训

升级 GCC 大版本要重扫头文件。CI 里加 -Wall -Wextra -Werror 能提前发现。


四、坑 4:protobuf 生成的 *.pb.cc 漂移

现象

1
fatal error: rpc_service.pb.h: No such file or directory

根因

我之前 commit 了一个旧版 *.pb.h,但 cmake 重跑时 protoc 重新生成了不同名字的——/build/rpc_service.pb.h 跟仓库里的 rpc_service.pb.h 内容不一致。

修法

删除仓库里所有 *.pb.cc / *.pb.h——这些是生成产物,不该 commit。CMake 每次 build 时 protoc 重新生成。

1
2
*.pb.cc
*.pb.h

教训

任何"由代码生成的代码"都不该 commit——*.pb.* / *_generated.* / *_grpc.pb.go / node_modules/ 全部 .gitignore。


五、坑 5:examples/echo/echo_server.cc 用过期 Codec API

现象

1
error: no matching function for call to 'Codec::decode(Buffer&, RpcRequest&)'

根因

Codec 库之前是 1 参 API:decode(Buffer&) → DecodedPacket;重构后变 2 参:decode(Buffer&, DecodedPacket&)。examples 没同步更新。

修法

照新 API 重写 echo_server.cc,顺手用新的 encodeResponse():

1
2
3
4
5
6
7
8
// 旧
auto pkt = codec_.decode(buf);
// 新
DecodedPacket pkt;
if (codec_.decode(buf, pkt)) {
  // 处理 pkt
  codec_.encodeResponse(resp, buf);
}

教训

库改 API 时,examples 是最后改的——但 examples 又是新人第一个看的代码,应该最干净给 examples 配独立 CI(每次 cmake --build 后跑一遍 example)。


六、坑 6:CMake 找 binary 路径不固定

现象

1
COPY failed: stat /src/build/examples/echo/echo_server: no such file or directory

根因

CMake 默认 build 在 build/,但子目录里有自己的 CMakeLists 时,可能变成 build/<sub>/<sub>/binary——examples/echo/CMakeLists.txt 写到 build/echo/,但 binary 在 build/echo/echo_server,examples/ 前缀没了

修法

find 探,不写死路径:

1
COPY --from=builder $(find /src/build -name 'echo_server' -type f -executable) /echo_server

教训

CI/CD 里的路径不要 hardcode——加一个 sub project 就崩。find / $(wildcard) / $(shell) 探真实路径


七、坑 7:stage-2 base 缺 GLIBCXX_3.4.32

现象

镜像跑起来,程序秒崩:

1
2
/echo_server: /lib/x86_64-linux-gnu/libstdc++.so.6: version 
`GLIBCXX_3.4.32' not found

根因

GCC 13 编译的 C++ 代码用了新的 std::format / 一些 STL 内部 API,需要 GLIBCXX_3.4.32debian:bookworm-slim 默认 libstdc++ 是 12 版本,不够新。

修法

静态链接 libstdc++ + libgcc——binary 自包含,不依赖 base image 的 libstdc++:

1
2
# CMake
target_link_options(echo_server PRIVATE -static-libstdc++ -static-libgcc)

教训

GCC 13 + 老 base image,必须 -static-libstdc++ -static-libgcc。要不就用 debian:sid (有新版 libstdc++) 配 apt 装,但镜像大 100MB+。


八、坑 8:docker-compose.yml 端口 vs binary listen 端口不一致

现象

502 Bad Gateway。docker compose ps 显示 echo-server Up,但 rpc-proxy 根本没起来

根因

  • docker-compose.yml:echo-server.expose: "8888"
  • docker-compose.yml:rpc-proxy.environment.ECHO_PORT: "8888"
  • 但 echo_server.cc 里 htons(9000) —— binary 听 9000,compose 配 8888

即使 rpc-proxy 起来,连 echo-server:8888 = 拒绝连接 → 502

修法

两套改一套——我选改 compose(不改业务代码):

1
2
3
4
5
6
7
8
services:
  echo-server:
    expose:
      - "9000"     # 改这里
  rpc-proxy:
    environment:
      ECHO_HOST: echo-server
      ECHO_PORT: "9000"  # 改这里

教训

deploy 配置 double check——compose、nginx、env 三处端口必须对齐。EXPOSE 变量避免 hardcode。


九、坑 9:healthcheck 用 python3debian-slim 没装

现象

容器 Up,但 STATUS 永远 (unhealthy):

1
2
"ExitCode": 127,
"Output": "/bin/sh: 1: python3: not found"

根因

compose healthcheck 写的是 python3 -c "import socket..."debian:bookworm-slim 默认没装 python3——ExitCode 127 = command not found。

修法

bash /dev/tcp/host/port(纯 bash,无依赖):

1
2
healthcheck:
  test: ["CMD-SHELL", "timeout 2 bash -c 'exec 3<>/dev/tcp/127.0.0.1/9000 && exec 3<&-' || exit 1"]

关键:exec 3<>/dev/tcp/... 立即 connect,然后 exec 3<&- 立即关——不要 cat < /dev/tcp/...,那是等数据,会 hang 到 timeout。

教训

healthcheck 命令要在目标镜像里"100% 可用"debian-slim 没 python3 / curl / nc,只能 bash /dev/tcp。要装就要在 stage-2 apt 装,但会让镜像大 30MB+


十、总结:9 个坑的根因分布

类型教训一句话
网络1, 2国内不用 gcr.io / build/ 必加 .gitignore
编译3, 4, 5, 6GCC 13 要 cstdint / 生成产物不 commit / examples 跟库同步 / 路径别写死
运行时7, 9静态链 / healthcheck 别用 python3
配置8deploy 配置三处对齐

十一、最终文件清单(可照搬)

Dockerfile.echo(2 阶段)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 阶段 1: builder
FROM gcc:13-bookworm AS builder
RUN apt-get update && apt-get install -y cmake git protobuf-compiler
WORKDIR /src
COPY . .
RUN for f in $(find . -name '*.cc' -o -name '*.h'); do
        grep -q '#include <cstdint>' "$f" || sed -i '1i #include <cstdint>' "$f"
    done
RUN mkdir -p build && cd build && \
    cmake -DCMAKE_BUILD_TYPE=Release -DRPC_SILENT=ON \
          -DCMAKE_EXE_LINKER_FLAGS="-static-libstdc++ -static-libgcc" .. && \
    make -j4 echo_server

# 阶段 2: runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-installs libprotobuf32 \
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder $(find /src/build -name 'echo_server' -type f -executable) /echo_server
EXPOSE 9000
CMD ["/echo_server"]

docker-compose.yml(关键片段)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:
  echo-server:
    image: rpc-framework/echo-server:built
    expose: ["9000"]
    healthcheck:
      test: ["CMD-SHELL", "timeout 2 bash -c 'exec 3<>/dev/tcp/127.0.0.1/9000 && exec 3<&-' || exit 1"]
  
  rpc-proxy:
    environment:
      ECHO_HOST: echo-server
      ECHO_PORT: "9000"
    ports: ["127.0.0.1:8080:80"]

十二、这 9 坑复盘的最大教训

build 成功 ≠ 部署成功 ≠ 容器健康 ≠ 业务可用——四个 flag 都要看到。

  • [+] build N/N + ✔ Image ... Built → 镜像造好了
  • docker compose ps 没有 (unhealthy) → 容器没被 healthcheck 判死
  • 业务 log 有 listening on X → 业务进程起来了
  • curl .../api/echo 返回 200 + 正确 JSON → 真通了

任何只验其中一个就报"成功" = 浪费你时间。这 9 坑里我有 3 个坑是已经看到部分 flag 就说"通了",结果用户跟着部署踩了后面的坑。


相关阅读: