从 Dockerfile 困境到 Nixpacks 升华:一次 Next.js 图片优化服务部署失败的深度复盘

从 Dockerfile 困境到 Nixpacks 升华:一次 Next.js 图片优化服务部署失败的深度复盘

本文详细记录了一次在 CI/CD 流程中,将 Next.js 应用的部署策略从 Dokploy 内置的 Nixpacks 构建切换为自定义 Dockerfile + GitHub Actions 后,引发的严重图片显示问题

bbuoooou
Nov 22, 2025•11 min read

摘要

本文详细记录了一次在 CI/CD 流程中,将 Next.js 应用的部署策略从 Dokploy 内置的 Nixpacks 构建切换为自定义 Dockerfile + GitHub Actions 后,引发的严重图片显示问题。文章将深入剖析问题的表象(仅非 SVG 格式的光栅图片在使用 next/image 组件后无法加载),逐步排查并定位到问题的根源——即 Node.js 原生模块 sharp 在跨环境构建与部署时产生的二进制不兼容性。最终,文章将展示如何通过在 CI/CD 流程中回归使用 Nixpacks,以一种更声明式、更可靠的方式彻底解决问题,并总结其相对于手动维护 Dockerfile 的核心优势。

1. 问题表象:一则关于图片 URL 的奇案

在将 Next.js 应用的部署流程迁移至 GitHub Actions 并使用自定义 Dockerfile 构建镜像后,一个诡异的问题浮出水面:应用中的部分图片无法显示。

初步观察呈现出清晰的规律:

  • 能正常显示的图片:

    1. 所有 SVG 格式的图片,其 <img> 标签的 src 属性直接指向原始 URL,如 https://img.buoucoding.com/logo/postion.svg。

    2. 部分第三方托管的头像,其 <img> 标签同样直接使用原始 URL,如 https://avatars.githubusercontent.com/...。

  • 无法显示的图片:

    1. 所有 PNG、JPEG、WebP 等光栅图片,只要它们被包裹在 Next.js 的 <Image> 组件中,其 <img> 标签的 src 和 srcset 就会被转换为 Next.js 内部的图片优化路由,形如 /_next/image?url=https%3A%2F%2F...&w=640&q=75。

这一现象立刻将疑点指向了 Next.js 的核心功能之一:服务端图片优化服务。直接引用 URL 的图片由浏览器直接请求,绕过了我们的服务后端;而经过 /_next/image 路由的图片,则需要我们的 Next.js 应用在容器内成功接收请求、抓取原始图片、执行优化并返回处理后的图像数据。显然,正是这最后一步骤失败了。

2. 深度排查:层层剥离,直指核心

为了定位问题的根源,我们进行了一系列系统性的排查。

2.1 网络与配置层排查

  • 容器网络连通性:通过 docker exec 进入正在运行的容器,使用 curl 命令请求 /_next/image URL 中指向的原始图片地址。请求成功,排除了容器 DNS 解析、网络策略或外部防火墙问题。

  • Next.js 配置:检查 next.config.mjs 文件,确认 images.remotePatterns 配置已正确包含了所有外部图片源的域名。配置无误。

结论:问题并非出在外部网络连通性或应用的基础配置上,而是**应用在容器内的运行时环境(Runtime Environment)**出现了问题。

2.2 锁定嫌疑:sharp 与原生模块的诅咒

Next.js 的图片优化功能默认由一个高性能的 Node.js 库 sharp 提供。sharp 并非纯 JavaScript 库,它是一个原生 Node.js 模块(Native Node.js Module),其核心性能依赖于一个用 C++ 编写的底层库 libvips。

这意味着 sharp 的正常运行有两个苛刻的前提:

  1. 系统级依赖:运行环境中必须存在 libvips 等动态链接库。

  2. 二进制兼容性:npm/pnpm/yarn 在安装 sharp 时,会下载或编译一个与当前操作系统、CPU 架构和 C++ 标准库(如 glibc)版本严格匹配的二进制文件 (.node 文件)。

基于此,我们形成了核心假设:从 Dokploy 的 Nixpacks(一个受控且统一的构建环境)迁移到 GitHub Actions + Dockerfile(一个潜在的跨环境构建流程)后,sharp 的二进制兼容性遭到了破坏。

  • 构建环境 (Build Environment):GitHub Actions 的 ubuntu-latest Runner。

  • 运行环境 (Runtime Environment):Dockerfile 中指定的 node:18-slim (基于 Debian) 镜像。

pnpm install 在 GitHub Actions 的环境中为 sharp 下载了适配 Ubuntu 的二进制文件。然而,当我们通过 COPY --from=builder /app/node_modules ./node_modules 将其复制到基于 Debian 的 runner 镜像中时,由于底层系统库(尤其是 glibc 版本)的细微差异,这个预编译的二进制文件无法被 Node.js 正常加载和执行,导致 /_next/image 服务静默失败。

3. Dockerfile 的救赎之路:从依赖注入到就地重生

明确了问题根源后,我们尝试通过修改 Dockerfile 来修复问题。

3.1 尝试一:注入系统依赖

最初的尝试是在 runner 镜像中简单地安装 sharp 的运行时依赖。

codeDockerfile

# runner 阶段
RUN apt-get update && apt-get install -y libvips && rm -rf /var/lib/apt/lists/*

结果:失败。这仅仅解决了运行时依赖缺失的问题,但无法解决核心的二进制不兼容问题。

3.2 尝试二:就地重新编译

这是解决原生模块跨环境问题的“银弹”。思路是在 runner 镜像中,强制 pnpm 丢弃旧的二进制文件,并使用当前环境的工具链重新编译 sharp。

codeDockerfile

# runner 阶段

# 1. 安装完整的编译工具链和运行时依赖
RUN apt-get update && apt-get install -y \
    build-essential \
    libvips \
    # ...其他依赖
    && rm -rf /var/lib/apt/lists/*

# ... 复制 node_modules ...

# 2. 强制重新编译 sharp
RUN pnpm rebuild sharp

在实践中,这一步还遇到了 pnpm: not found 的错误,因为忘记了在 runner 阶段重新通过 corepack enable 启用 pnpm。修正后,这个方案理论上是可行的。

4. 终极方案:回归 Nixpacks,拥抱声明式构建

尽管通过手动修改 Dockerfile 可以解决问题,但这个过程暴露了其内在的脆弱性:

  • 高维护成本:开发者必须手动追踪并管理所有原生模块的深层系统依赖。

  • 环境耦合:Dockerfile 的内容与 CI/CD 的基础镜像环境紧密耦合。

  • 隐性知识:需要开发者具备关于原生模块编译、Linux 系统库等底层知识。

此时,我们反思:为什么 Dokploy 的 Nixpacks 从一开始就没有这个问题?

答案在于 Nixpacks 的设计哲学。它是一个智能化的应用构建器,内置了对主流框架(如 Next.js)的“知识库”(Providers)。当你把项目交给 Nixpacks 时,它会自动:

  1. 识别项目类型:检测到这是一个 Next.js 应用。

  2. 应用预设方案:从其 Provider 知识库中得知 next/image 依赖 sharp,而 sharp 需要 libvips 和 build-essential。

  3. 注入所有依赖:在一个统一、一致的环境中,自动安装所有必要的系统级和应用级依赖,并完成构建。

这从根本上避免了我们手动操作时遇到的跨环境二进制不兼容问题。

因此,最终的、最优雅的解决方案是在 GitHub Actions 中直接使用 Nixpacks,将成功的构建逻辑从 Dokploy 移植到我们的 CI/CD 流程中。

最终的 GitHub Action 工作流 (.github/workflows/main.yml)

name: Build and Push Docker Image with Nixpacks

on:
  push:
    branches: [ main ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push with Nixpacks
        # 使用社区验证的 Nixpacks Action,它封装了构建和推送的逻辑
        uses: iloveitaly/github-action-nixpacks@main
        with:
          push: true
          tags: |
            ghcr.io/your-username/your-repo:latest
            ghcr.io/your-username/your-repo:${{ github.sha }}

      - name: Trigger Dokploy Redeploy
        run: curl -X POST ${{ secrets.DOKPLOY_WEBHOOK_URL }}

通过这个工作流,我们彻底删除了手动维护的 Dockerfile,将构建的复杂性完全委托给 Nixpacks。

结论

手动编写 Dockerfile 提供了无与伦比的灵活性,但也要求开发者对应用的整个依赖栈有深刻的理解,尤其是在处理包含原生模块的复杂应用时。本次 Next.js 图片优化服务的部署失败,是一个典型的因构建与运行环境不匹配导致原生模块二进制不兼容的案例。

最终,通过在 CI/CD 流程中采用 Nixpacks 这样的高级构建工具,我们将构建过程从**指令式(Imperative)的“食谱编写”转变为声明式(Declarative)**的“目标描述”。这不仅解决了眼前的问题,还极大地简化了 CI/CD 配置,降低了长期维护成本,并提高了部署流程的健壮性。对于现代 Web 应用开发而言,拥抱这类智能化、自动化的构建工具无疑是提升开发效率和部署可靠性的明智之选。

Comments (0)