Docker + Rust 构建问题分析

Docker + Rust 构建问题分析

问题

  • 容器启动后立即退出 (exited with code 0)
  • 完全没有任何日志输出(连 main() 第一行都没打印)
  • 改小版本号无效 (v3.1.9 → v3.2.0 都失败)
  • 改大版本号就好了 (v3.x → v4.0.0 突然成功)
  • 不同机器构建结果不一致(本地失败,服务器成功)

关键线索:二进制文件大小

正常启动的:22MB ~ 24MB
启动失败的:443KB ~ 450KB  ← 这是损坏的!

根本原因

Cargo 增量编译 + Docker 缓存的潜在问题

Dockerfile 中常见的"依赖缓存优化"会导致问题:

# 步骤 1:缓存依赖
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release
# ↑ 这里生成了一个 450KB 的 dummy 二进制

# 步骤 2:删除假源码
RUN rm -rf src

# 步骤 3:复制真实源码
COPY . .

# 步骤 4:真正构建
RUN cargo build --release
# ↑ 问题出现在这里!

关键问题

  1. Cargo 检测到 target/release/ 已存在 dummy 二进制
  2. 增量编译判断:源码没变化(因为 Docker 缓存)
  3. Cargo 决定:不需要重新编译,直接复用旧二进制
  4. 结果:最终镜像里是那个 450KB 的废物二进制!

为什么版本号变更有时能解决问题?

v3.1.9 → v3.2.0 无效

Docker 认为这是"小改动"
→ 继续使用缓存层
→ 继续使用损坏的二进制

v3.x → v4.0.0 有效

Docker 认为这是"重大变更"(主版本号变化)
→ 某些缓存策略被重置
→ 意外触发了完全重新构建
→ 生成了正确的 24MB 二进制

但这不是可靠的修复!下次 v4.1.0 可能又会失败!


为什么这个问题如此隐蔽?

1. 编译成功,没有错误

[builder 8/8] RUN cargo build --release0.6s

2. 容器启动成功,立即退出

solji_indexer_sonia exited with code 0
  • exit 0 = “正常退出”(误导性结果!)
  • 实际:二进制损坏,连 main() 都没执行

3. 完全没有日志

fn main() {
    println!("Starting..."); // ← 这行永远不会执行
}

因为二进制根本就是假的 fn main() {}

4. 本地和服务器表现不一致

  • 本地 Mac (arm64):缓存污染,生成 450KB
  • 服务器 (amd64):重新构建,生成 107MB
  • 造成错觉:“是不是架构问题?”

最终解决方案

核心思路:强制清理旧二进制,禁止增量编译复用

FROM rust:1.83-bookworm AS builder

RUN apt-get update && apt-get install -y \
    pkg-config libssl-dev build-essential ca-certificates \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# ===== 步骤 1:缓存依赖(这个没问题)=====
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build --release --locked

# ===== 步骤 2:清理旧二进制(关键!)=====
RUN rm -rf target/release/solji-indexer \
           target/release/deps/solji_indexer* \
           target/release/.fingerprint/solji-indexer-*

# ===== 步骤 3:复制真实代码 =====
COPY . .

# ===== 步骤 4:完整构建 =====
RUN cargo build --release --locked

# ===== 步骤 5:验证二进制大小(重要!)=====
RUN SIZE=$(stat -c%s target/release/solji-indexer 2>/dev/null || stat -f%z target/release/solji-indexer) && \
    echo "Binary size: $SIZE bytes" && \
    if [ $SIZE -lt 5000000 ]; then \
        echo "ERROR: Binary too small ($SIZE bytes), build failed!" && exit 1; \
    fi

# ===== Runtime 镜像 =====
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
    libssl3 ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/solji-indexer /usr/local/bin/solji-indexer
COPY .env /usr/local/bin/.env

WORKDIR /usr/local/bin
EXPOSE 8080

CMD ["/usr/local/bin/solji-indexer"]

防御措施

1. 构建时验证二进制大小

RUN SIZE=$(stat -c%s target/release/solji-indexer) && \
    [ $SIZE -gt 5000000 ] || (echo "Build failed: binary too small" && exit 1)

2. 使用 --locked 参数

RUN cargo build --release --locked

确保使用 Cargo.lock 中的精确版本

3. 构建后立即测试

# 在 builder 阶段测试二进制能否运行
RUN ./target/release/solji-indexer --version || \
    (echo "Binary is broken!" && exit 1)

4. 清理 Docker 缓存

# 每次重要构建前执行
docker builder prune -af
docker build --no-cache -t myimage:tag .

5. CI/CD 中固定构建环境

# GitHub Actions
- name: Build Docker Image
  run: |
    docker build --no-cache \
      --build-arg BUILDKIT_INLINE_CACHE=1 \
      -t sonia831/solji-indexer:${{ github.sha }} .

检测清单

# 1. 检查镜像大小
docker images | grep solji-indexer
# 应该 > 100MB

# 2. 检查二进制大小
docker run --rm --entrypoint ls myimage:tag -lh /usr/local/bin/solji-indexer
# 应该 > 20MB

# 3. 测试启动
docker run --rm myimage:tag --version
# 应该有输出

# 4. 查看依赖库
docker run --rm --entrypoint ldd myimage:tag /usr/local/bin/solji-indexer
# 应该能看到 libssl, libc 等