首页 / 文章

解决 Alpine Docker 上 Prisma 的 libssl.so.1.1 库缺失错误

在缺少 libssl.so.1.1 的 Alpine Docker 镜像上,Prisma 的查询引擎会崩溃。OpenSSL 实际上存在什么问题?如何在 Node.js 中彻底解决此问题?

发布者:REACTAPP.TOP阅读约 6 分钟1479 词

来源: Prisma Docker Error: `libssl.so.1.1` Not Found on Alpine

你编写了 Dockerfile,启动了构建过程,一切似乎都很正常。然后你运行了 docker compose up,观察日志流,就在应用程序应该连接到数据库的那一刻,它崩溃了。

症状

如果出现以下任何情况,其原因及解决方法如下:

  • 崩溃报告称在文件系统中找不到名为 libssl.so.1.1 的共享库
  • Prisma 启动失败,显示 PrismaClientInitializationError: Unable to require libquery_engine-linux-musl.so.node
  • 有消息提示 Prisma 无法确定应使用哪个版本的 libssl/openssl
  • 通过 docker compose up 启动的容器几乎立即关闭
  • 构建过程顺利完成,没有任何问题,但容器一旦尝试与数据库通信就会崩溃
  • 完整的错误信息通常如下所示:

    Details: Error loading shared library libssl.so.1.1: No such file or directory
    (needed by /app/node_modules/.prisma/client/libquery_engine-linux-musl.so.node)
    

    堆栈跟踪

    出现此问题的场景是:使用基于 TypeScript 的 Node.js/Express API,ORM 层为 Prisma,数据库为 PostgreSQL。Dockerfile 基于 node:20-alpine 制定,这是一个合理的默认选择,因为基于 Alpine 的镜像比基于 Debian 的镜像轻很多——下载速度更快,漏洞风险更低,且更适合作为生产环境部署使用。

    构建过程本身没有出现问题。这是一个多阶段的设置:TypeScript被编译到dist/目录中,Prisma Client也成功生成。构建过程中没有出现任何错误。问题只在执行docker compose up启动容器时才出现。

    实际情况是怎样的

    Alpine Linux是被刻意简化过的。这正是它的优势所在——也正是因此生成的镜像体积如此之小。但正因为过于精简,Alpine省略了许多Debian或Ubuntu基础镜像默认包含的软件包。

    Prisma 的查询引擎以预编译的二进制文件形式提供——即随项目一同打包的 .so(共享对象)文件,在此情况下其名称类似 libquery_engine-linux-musl.so.node。该二进制文件负责与数据库进行实际通信,且其运行直接依赖于 OpenSSL。

    关键在于,此引擎二进制文件是针对某个特定版本的 OpenSSL 进行链接的,并非泛指所有的 OpenSSL。它所依赖的版本取决于 Prisma 的版本以及 Alpine 系统的版本:

    • 早期版本的 Prisma 引擎是针对 OpenSSL 1.1 进行链接的,该版本会提供 libssl.so.1.1
    • 较新版本的 Prisma 是针对 OpenSSL 3 进行构建的,该版本会提供 libssl.so.3
    • 截至 3.16 版本的 Alpine 系统默认使用 OpenSSL 1.1
  • Alpine 3.17 及更高版本则使用 OpenSSL 3。
  • 因此,您遇到的具体错误取决于所使用的 Prisma 版本和 Alpine 版本的组合。请仔细阅读错误信息——其中会指出缺失的具体共享库,而 libssl.so.1.1 与 libssl.so.3 对应的缺失库需要不同的软件包以及不同的修复方法。

    这个问题为何要在运行时才出现,而非在构建阶段就失败,是因为在构建阶段执行的 prisma generate 命令实际上从未运行过查询引擎的二进制文件,它仅生成 TypeScript 客户端代码。直到应用程序启动且 Prisma 尝试建立数据库连接时,引擎二进制文件才会被调用。此时您已经处于一个包含不兼容或缺失 OpenSSL 库的运行中的容器中,故障发生前不会再有任何警告。

    解决方案

    解决方法是通过 Alpine 的包管理器 apk 在 Dockerfile 中显式安装 OpenSSL。需要哪个包完全取决于错误信息中的提示。

    如果错误信息中提到 libssl.so.1.1(OpenSSL 1.1):

    在较新的 Alpine 版本(3.17 及以上)中,OpenSSL 1.1 已不再默认安装,因此需要相应的兼容包:

    RUN apk add --no-cache openssl1.1-compat
    

    如果错误信息中提到 libssl.so.3(OpenSSL 3),或者你正在较新的 Alpine 版本上运行最新的 Prisma 版本:

    RUN apk add --no-cache openssl
    

    这将加载 Alpine 包仓库中该版本提供的 OpenSSL 构建版本——在 Alpine 3.17 及以上版本中,即为 OpenSSL 3。

    最安全的长期策略是刻意锁定版本。通过基础镜像标签修复 Alpine 版本,并在 package.json 中固定 Prisma 版本,这样 OpenSSL 的要求就能保持一致,不会在不同环境中出现意外变化。Prisma 自身的发布说明会列出每个引擎二进制文件所基于的 OpenSSL 版本。

    如果你的构建流程采用多阶段构建,那么需要在两个阶段都添加这条指令,因为每个阶段都是在独立的 Alpine 环境中运行的:

    # Stage 1: Builder
    FROM node:20-alpine AS builder
    RUN apk add --no-cache openssl
    
    WORKDIR /app
    COPY package*.json ./
    COPY prisma ./prisma
    RUN npm install
    COPY . .
    RUN npx prisma generate
    RUN npm run build
    
    # Stage 2: Production
    FROM node:20-alpine AS production
    RUN apk add --no-cache openssl
    
    WORKDIR /app
    ENV NODE_ENV=production
    
    COPY package*.json ./
    COPY prisma ./prisma
    RUN npm install --omit=dev
    COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
    COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
    COPY --from=builder /app/dist ./dist
    
    EXPOSE 4000
    CMD ["node", "dist/server.js"]
    

    --no-cache 参数可防止 apk 保存包索引的本地副本,从而保持最终镜像的大小较小——这正是你最初选择 Alpine 的原因所在。

    为何不直接改用 node:slim?

    这确实是个值得探讨的问题。node:20-slim是基于Debian而非Alpine构建的,而Debian默认就自带OpenSSL,因此这种特定的故障模式几乎不会出现。很多团队直接选择切换到该版本并继续使用。

    不过问题在于镜像大小。基于Alpine的典型Node镜像大小在50到70MB左右,而Debian slim版本的镜像大小则接近150到200MB。如果要在数十次部署、多个服务中使用,再加上团队成员反复拉取镜像,这些数据量会迅速累积。此外,Alpine默认的攻击面更小,对于那些对安全性要求较高的环境来说,这是一个重要的优势。

    通过 apk add 安装缺失的包是一种常见的方法,Prisma 的指南中有相关说明,而且许多实际使用的 Dockerfile 也已采用此方式。为避免版本匹配问题而转用基于 Debian 的镜像也是一个完全合理的选择,但无论最终选用哪种基础镜像,了解版本不匹配的成因仍然是必要的。

    关于多阶段构建的说明

    如果之前的 Dockerfile 中的两行 FROM 语句以及 --from=builder 语法让你感到陌生,下面简要说明这种设计背后的原理。

    一个仅包含单个阶段且用于编译 TypeScript 的 Dockerfile 会将 TypeScript 编译器、ts-node、所有开发依赖项以及原始源文件一并打包到生产镜像中。这些内容根本不应出现在其中——它们不仅会增加镜像体积,还会扩大潜在安全漏洞的暴露面。

    多阶段构建方式能够将各部分妥善分离:构建阶段包含编译项目所需的所有内容,而生产阶段则从干净的基镜像开始,仅加载运行时实际需要的组件——即编译后的 dist/ 目录内容、仅用于生产环境的依赖项以及生成的 Prisma Client。当 Docker 组装最终镜像时,构建阶段的所有冗余内容会自动被剔除。

    这样最终得到的就是一个精简且可直接用于生产的镜像,无需携带自身的构建工具链。

    修复之后

    安装了正确的 OpenSSL 版本后,运行 docker compose up 应该就能得到你期望的输出结果:

    ✅ Connected to PostgreSQL
    🚀 Server listening on port 4000
    

    如果你的 Prisma 配置是通过 Docker Compose 运行的,接下来需要处理的是针对容器化的数据库应用迁移,而非本地运行的数据库。简而言之,就是执行 docker compose exec app npx prisma migrate deploy,该命令会在正在运行的应用容器内部执行迁移操作——此时 DATABASE_URL 指向的是由 Compose 管理的 Postgres 服务,而非主机上的其他数据库。

    第一次遇到 OpenSSL 错误时,那堆以 ENOENT 结尾的复杂二进制输出信息确实会让人望而却步。但根本原因其实很简单且具体:Prisma 的查询引擎是基于特定版本的 OpenSSL 编译而成的二进制文件,而 Alpine 并不能保证该版本确实存在。仔细阅读错误信息,弄清楚它需要的是哪个版本的 libssl,然后相应地安装该版本,并锁定版本信息,这样无论容器在何处运行,其行为都能保持一致。