Устранение ошибки отсутствия библиотеки libssl.so.1.1 в Prisma в среде Alpine Docker
Механизм запросов Prisma выходит из строя в образах Alpine Docker из-за отсутствия файла libssl.so.1.1. Что на самом деле происходит с OpenSSL и как навсегда решить эту проблему в Node.js.
Источники: 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)
Стек вызовов
Ситуация, при которой возникла эта проблема, связана с API на TypeScript в Node.js/Express, Prisma в качестве слоя ORM и PostgreSQL в качестве базы данных. Dockerfile был основан на node:20-alpine — разумном стандартном варианте, поскольку образы на базе Alpine значительно легче аналогов на Debian: их быстрее загружают, у них меньше возможностей для появления уязвимостей, и они в целом лучше подходят для развертывания в производстве.
Сам процесс сборки пройдёт без проблем. Это была многоэтапная настройка: TypeScript скомпилировался в папку dist/, а клиент Prisma был корректно сгенерирован. Во время сборки ошибок не было. Проблемы возникли только тогда, когда команда 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 - Версии Alpine до 3.16 по умолчанию содержали OpenSSL 1.1
Поэтому конкретная ошибка зависит от того, какие версии Prisma и Alpine вы используете вместе. Внимательно прочитайте текст ошибки — там указана конкретная отсутствующая библиотека, причем libssl.so.1.1 и libssl.so.3 требуют разных пакетов и разных способов решения проблемы.
Причина, по которой ошибка проявляется только во время выполнения программы, а не на этапе сборки, заключается в том, что команда prisma generate, которая выполняется на этапе сборки, на самом деле никогда не запускает бинарник движка запросов. Она лишь генерирует клиентский код на TypeScript. Сам бинарник движка запускается только тогда, когда приложение начинает работу и Prisma пытается установить соединение с базой данных. К тому моменту вы уже находитесь в запущенном контейнере с несовместимой или отсутствующей библиотекой OpenSSL, и предупреждений больше нет до тех пор, пока всё не сломается.
Решение
Решение заключается в явной установке OpenSSL в ваш Dockerfile с помощью менеджера пакетов Alpine — apk. Какой именно пакет вам нужен, полностью зависит от информации, указанной в сообщении об ошибке.
Если в ошибке упоминается 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), или вы используете новую версию Prisma на современной версии Alpine:
RUN apk add --no-cache openssl
Это загружает ту версию OpenSSL, которая предоставлена репозиторием пакетов Alpine для данной версии — в Alpine 3.17+ это OpenSSL 3.
Самая надежная долгосрочная стратегия — это целенаправленное фиксирование версий компонентов. Используйте тег базового образа для установки нужной версии Alpine, а также задайте конкретную версию Prisma в файле package.json, чтобы требования к 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 поставляется из коробки, поэтому этот конкретный способ сбоя в основном исчезает. Многие команды просто делают этот переход и продолжают работу.
Проблема заключается в размере изображений. Типичный образ Node на базе Alpine занимает примерно 50–70 МБ, в то время как аналогичный образ на Debian slim занимает около 150–200 МБ. Если учесть десятки развертываний, несколько сервисов и постоянные загрузки всеми членами команды, это быстро накапливается. Кроме того, Alpine по умолчанию имеет меньшую площадь атаки, что является важным фактором при работе в средах с высокими требованиями к безопасности.
Установка отсутствующего пакета с помощью apk add — это распространённый способ, прямо описанный в руководствах Prisma, и он уже используется во множестве производственных Dockerfile. Переход на образ, основанный на Debian, чтобы избежать проблем с совместимостью версий, — вполне разумный выбор, но всё равно стоит понять, почему возникает такая несовместимость вообще, независимо от выбранного базового образа.
Примечание о многоступенчатой сборке
Если две строки FROM и синтаксис --from=builder в предыдущем Dockerfile показались вам незнакомыми, вот краткое объяснение принципа их использования.
Dockerfile с одним этапом, в котором компилируется TypeScript, приводит к тому, что компилятор TypeScript, ts-node, все devDependency и сырые исходные файлы попадают непосредственно в производственный образ. Ничто из этого не должно там находиться — это увеличивает размер образа и расширяет поверхность, подверженную потенциальным угрозам безопасности.
Многоэтапная сборка позволяет правильно разделить компоненты: этап сборки содержит всё необходимое для компиляции проекта, в то время как производственный этап начинается с чистой основы и включает только то, что действительно требуется во время выполнения — скомпилированные файлы из папки 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 указывает на сервис Postgres, управляемый Compose, а не на какой-либо ресурс на хост-машине.
Ошибка OpenSSL пугает при первой встрече с ней — это длинный блок бинарных данных, заканчивающийся кодом ENOENT. Однако корень проблемы заключается в чем-то конкретном: движок запросов Prisma представляет собой скомпилированный бинарный файл, созданный для определённой версии OpenSSL, а Alpine не гарантирует наличие именно этой версии. Внимательно прочитайте сообщение об ошибке, выясните, какую библиотеку libssl оно требует, установите её соответственно и зафиксируйте версии компонентов, чтобы поведение системы оставалось предсказуемым независимо от места запуска контейнера.