Solución del error de biblioteca faltante libssl.so.1.1 en Prisma en Alpine Docker
El motor de consultas de Prisma se cae en imágenes Docker Alpine cuando falta libssl.so.1.1. ¿Qué está sucediendo realmente con OpenSSL y cómo solucionarlo de forma permanente en Node.js?
Fuentes: Prisma Docker Error: `libssl.so.1.1` Not Found on Alpine
Escribió el Dockerfile, inició la compilación y todo parecía estar bien. Luego ejecutó docker compose up, observó cómo pasaba el flujo de registros y, justo en el momento en que la aplicación debería haberse conectado a la base de datos, falló.
Síntomas
Si alguno de estos le resulta familiar, la explicación y la solución se encuentran a continuación:
- Un informe de fallo que indica que no se pudo localizar en ningún lugar del sistema de archivos una biblioteca compartida llamada
libssl.so.1.1 - Un error al iniciar Prisma que muestra
PrismaClientInitializationError: Unable to require libquery_engine-linux-musl.so.node - Un mensaje que indica que Prisma no pudo determinar qué versión de libssl/openssl se está utilizando
- El contenedor iniciado con
docker compose upse cierra casi de inmediato
El error completo suele verse más o menos así:
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)
La pila de ejecución
El escenario en el que surgió este problema involucraba una API de Node.js/Express basada en TypeScript, Prisma como capa ORM y PostgreSQL como base de datos. El Dockerfile se basaba en node:20-alpine, una opción por defecto razonable, ya que las imágenes basadas en Alpine son considerablemente más ligeras que sus equivalentes de Debian: se descargan más rápido, tienen menos superficie para vulnerabilidades y son en general más adecuadas para despliegues en producción.
La compilación en sí se realizó sin problemas. Fue un proceso en varias etapas: TypeScript se compiló hasta dist/, y el Prisma Client se generó correctamente. No hubo errores durante la compilación. Los problemas solo aparecieron cuando docker compose up ejecutó realmente el contenedor.
¿Qué está sucediendo realmente?
Alpine Linux está diseñado de forma intencional para ser muy ligero. Ese es todo su atractivo: por eso las imágenes resultantes son tan pequeñas. Pero al ser mínimo, Alpine omite muchos paquetes que las imágenes basadas en Debian o Ubuntu incluyen por defecto.
El motor de consultas de Prisma se entrega como un binario precompilado: un archivo .so (objeto compartido) incluido en tu proyecto, denominado algo como libquery_engine-linux-musl.so.node en este contexto. Este binario es el responsable de la comunicación real con tu base de datos, y depende directamente de OpenSSL para poder ejecutarse.
Lo importante aquí es que este binario del motor está vinculado a una versión *específica* de OpenSSL, no a OpenSSL en general. La versión que espera depende tanto de la versión de Prisma como de la versión de Alpine:
- Las versiones anteriores del motor de Prisma estaban vinculadas a OpenSSL 1.1, que expone
libssl.so.1.1 - Las versiones más recientes de Prisma están desarrolladas con OpenSSL 3, que expone
libssl.so.3 - Las versiones de Alpine hasta 3.16 ven OpenSSL 1.1 como predeterminado
Por eso, el error específico que se presenta depende de las versiones de Prisma y Alpine que se combinen. Lea atentamente el texto del error: indica la biblioteca compartida concreta que falta, y libssl.so.1.1 frente a libssl.so.3 requieren un paquete distinto y una solución diferente.
La razón por la que esto no se detecta hasta el tiempo de ejecución, en lugar de causar un fallo durante la compilación, es que prisma generate, que se ejecuta en la fase de compilación, nunca ejecuta realmente el binario del motor de consultas. Solo genera el código cliente en TypeScript. El propio binario del motor no se invoca hasta que la aplicación se inicia y Prisma intenta establecer una conexión a la base de datos. Para entonces ya se está dentro de un contenedor en ejecución con una biblioteca OpenSSL incompatible o ausente, y no hay más advertencias antes de que surjan los problemas.
La solución
La solución consiste en instalar explícitamente OpenSSL en su Dockerfile a través del gestor de paquetes de Alpine, apk. El paquete que necesite depende por completo de lo que le indique el mensaje de error.
Si el error menciona libssl.so.1.1 (OpenSSL 1.1):
En las versiones más recientes de Alpine (3.17 en adelante), OpenSSL 1.1 ya no se instala por defecto, por lo que necesita el paquete de compatibilidad:
RUN apk add --no-cache openssl1.1-compat
Si el error menciona libssl.so.3 (OpenSSL 3), o si está ejecutando una versión reciente de Prisma en una versión reciente de Alpine:
RUN apk add --no-cache openssl
Esto carga la versión de OpenSSL que proporciona el repositorio de paquetes de Alpine para esa versión; en Alpine 3.17 y superiores, se trata de OpenSSL 3.
La estrategia a largo plazo más segura es fijar deliberadamente sus versiones. Corrija su versión de Alpine a través de la etiqueta de imagen base y fije su versión de Prisma dentro de package.json, para que el requisito de OpenSSL se mantenga consistente y no cambie inesperadamente entre entornos. Las notas de lanzamiento de Prisma mismas indican contra qué versión de OpenSSL se compila cada binario del motor.
Si su configuración utiliza una compilación en múltiples etapas, debe agregar esta línea a ambas etapas, ya que cada una se ejecuta de forma independiente en 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"]
La bandera --no-cache impide que apk guarde una copia local del índice de paquetes, lo que mantiene el tamaño final de la imagen pequeño, en línea con la razón principal por la que eligió Alpine desde un principio.
¿Por qué no simplemente cambiar a node:slim?
Esa es una pregunta válida. node:20-slim está basado en Debian y no en Alpine, y Debian incluye OpenSSL de forma predeterminada, por lo que este modo específico de fallo desaparece en gran medida. Muchos equipos simplemente realizan ese cambio y siguen adelante.
El problema radica en el tamaño de las imágenes. Una imagen típica de Node basada en Alpine tiene un tamaño de entre 50 y 70 MB, mientras que la versión equivalente de Debian slim alcanza entre 150 y 200 MB. Si se multiplica esa diferencia en decenas de despliegues, varios servicios y las descargas repetidas por todos los miembros del equipo, el volumen total aumenta rápidamente. Además, Alpine cuenta con una superficie de ataque más reducida por defecto, lo cual es un factor importante si se trabaja en un entorno sensible desde el punto de vista de la seguridad.
Instalar el paquete faltante mediante apk add es un método muy utilizado, documentado directamente en las guías de Prisma, y ya se puede encontrar en innumerables Dockerfiles de producción. Pasar a una imagen basada en Debian para evitar los problemas relacionados con la compatibilidad de versiones es una opción perfectamente válida, pero sigue siendo útil comprender por qué ocurre esa incompatibilidad en primer lugar, independientemente de la imagen base que se elija.
Una nota sobre las construcciones multietapa
Si las dos líneas FROM y la sintaxis --from=builder del Dockerfile anterior le resultaron desconocidas, aquí hay una breve explicación de la lógica detrás de ese patrón.
Un Dockerfile con una sola etapa que compila TypeScript termina incluyendo el compilador de TypeScript, ts-node, todas las dependencias de desarrollo y tus archivos de código fuente directamente en la imagen de producción. Nada de eso debería estar allí, ya que hace que la imagen sea más grande y aumenta las posibilidades de exposición a vulnerabilidades.
Una construcción multietapa mantiene todo bien separado: la etapa de compilación contiene todo lo necesario para compilar el proyecto, mientras que la etapa de producción parte de una base limpia y solo incluye lo que realmente se necesita en tiempo de ejecución: los archivos compilados en dist/, las dependencias exclusivas para producción y el Cliente Prisma generado. Todo lo voluminoso de la etapa de compilación se deja atrás automáticamente una vez que Docker crea la imagen final.
Lo que obtienes al final es una imagen ligera y lista para producción que no lleva consigo su propia cadena de herramientas de compilación.
Después de la corrección
Con la versión correcta de OpenSSL instalada, ejecutar docker compose up debería finalmente proporcionarle el resultado que esperaba:
✅ Connected to PostgreSQL
🚀 Server listening on port 4000
A partir de ahí, si su configuración de Prisma se ejecuta a través de Docker Compose, lo siguiente que hay que resolver es aplicar las migraciones en la base de datos contenerizada en lugar de en lo que esté ejecutándose localmente. En resumen, eso significa ejecutar docker compose exec app npx prisma migrate deploy, lo cual ejecuta la orden de migración desde dentro del contenedor de la aplicación en ejecución, donde DATABASE_URL apunta al servicio Postgres gestionado por Compose y no a cualquier otro servicio en su máquina host.
El error de OpenSSL puede resultar intimidante la primera vez que lo encuentras: un bloque denso de salida en formato binario que termina con ENOENT. Pero la causa raíz es específica y sencilla: el motor de consultas de Prisma es un binario compilado para una versión concreta de OpenSSL, y Alpine no garantiza que esa versión esté realmente presente. Lee detenidamente el mensaje de error, determina qué libssl está solicitando, instálalo en consecuencia y establece versiones fijas para que el comportamiento sea predecible sin importar dónde se ejecute el contenedor.